209 lines
7.1 KiB
Python
209 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlmodel import Session
|
|
|
|
from ..connectors.deluge import DelugeConnector
|
|
from ..db.session import get_session
|
|
from ..services.approval_service import create_approval
|
|
from ..services.audit_logger import log_event
|
|
from ..services.job_runner import complete_job, create_job, set_job_progress, start_job
|
|
from ..services.policy_engine import evaluate_policy
|
|
from .connector_runtime import load_connector_runtime
|
|
|
|
router = APIRouter(prefix='/api/apps/deluge', dependencies=[Depends(load_connector_runtime)])
|
|
|
|
|
|
_PAUSE_STEPS = [
|
|
'Validate torrent id',
|
|
'Evaluate policy',
|
|
'Check Deluge health/session',
|
|
'Pause torrent',
|
|
'Fetch torrent status',
|
|
'Complete audit/job',
|
|
]
|
|
|
|
_RESUME_STEPS = [
|
|
'Validate torrent id',
|
|
'Evaluate policy',
|
|
'Check Deluge health/session',
|
|
'Resume torrent',
|
|
'Fetch torrent status',
|
|
'Complete audit/job',
|
|
]
|
|
|
|
_BULK_STEPS = [
|
|
'Validate action scope',
|
|
'Evaluate policy',
|
|
'Require operator approval',
|
|
'Call Deluge bulk operation',
|
|
'Complete audit/job',
|
|
]
|
|
|
|
|
|
@router.get('/health')
|
|
def health():
|
|
return DelugeConnector().health()
|
|
|
|
|
|
@router.get('/session')
|
|
def session_status():
|
|
return DelugeConnector().session_status()
|
|
|
|
|
|
@router.get('/torrents')
|
|
def torrents():
|
|
return DelugeConnector().torrent_status()
|
|
|
|
|
|
@router.get('/active')
|
|
def active():
|
|
return DelugeConnector().active_downloads()
|
|
|
|
|
|
@router.get('/stalled')
|
|
def stalled():
|
|
return DelugeConnector().stalled_downloads()
|
|
|
|
|
|
@router.get('/paused')
|
|
def paused():
|
|
return DelugeConnector().paused_downloads()
|
|
|
|
|
|
@router.get('/speeds')
|
|
def speeds():
|
|
return DelugeConnector().speed_status()
|
|
|
|
|
|
@router.post('/torrents/{torrent_id}/pause')
|
|
def pause_torrent(torrent_id: str, payload: dict | None = None, session: Session = Depends(get_session)):
|
|
return _execute_deluge_action(
|
|
session=session,
|
|
payload=payload or {},
|
|
action='deluge.pause_torrent',
|
|
steps=_PAUSE_STEPS,
|
|
connector_call=lambda connector: connector.pause_torrent(torrent_id),
|
|
default_success_message='torrent_paused',
|
|
metadata={'torrentId': torrent_id},
|
|
)
|
|
|
|
|
|
@router.post('/torrents/{torrent_id}/resume')
|
|
def resume_torrent(torrent_id: str, payload: dict | None = None, session: Session = Depends(get_session)):
|
|
return _execute_deluge_action(
|
|
session=session,
|
|
payload=payload or {},
|
|
action='deluge.resume_torrent',
|
|
steps=_RESUME_STEPS,
|
|
connector_call=lambda connector: connector.resume_torrent(torrent_id),
|
|
default_success_message='torrent_resumed',
|
|
metadata={'torrentId': torrent_id},
|
|
)
|
|
|
|
|
|
@router.post('/pause-all')
|
|
def pause_all(payload: dict | None = None, session: Session = Depends(get_session)):
|
|
return _execute_deluge_action(
|
|
session=session,
|
|
payload=payload or {},
|
|
action='deluge.pause_all',
|
|
steps=_BULK_STEPS,
|
|
connector_call=lambda connector: connector.pause_all(),
|
|
default_success_message='all_torrents_paused',
|
|
metadata={'scope': 'all'},
|
|
)
|
|
|
|
|
|
@router.post('/resume-all')
|
|
def resume_all(payload: dict | None = None, session: Session = Depends(get_session)):
|
|
return _execute_deluge_action(
|
|
session=session,
|
|
payload=payload or {},
|
|
action='deluge.resume_all',
|
|
steps=_BULK_STEPS,
|
|
connector_call=lambda connector: connector.resume_all(),
|
|
default_success_message='all_torrents_resumed',
|
|
metadata={'scope': 'all'},
|
|
)
|
|
|
|
|
|
def _execute_deluge_action(
|
|
*,
|
|
session: Session,
|
|
payload: dict[str, Any],
|
|
action: str,
|
|
steps: list[str],
|
|
connector_call: Callable[[DelugeConnector], dict[str, Any]],
|
|
default_success_message: str,
|
|
metadata: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
target = 'deluge'
|
|
source = str(payload.get('source', 'manual')).strip() or 'manual'
|
|
policy = evaluate_policy(session=session, action=action, target=target, source=source)
|
|
risk = str(policy.get('risk', 'medium'))
|
|
|
|
if policy['decision'] == 'blocked':
|
|
audit = log_event(
|
|
session,
|
|
source='action',
|
|
identity=source,
|
|
action_id=action,
|
|
target=target,
|
|
risk=risk,
|
|
policy_decision='blocked',
|
|
reason=policy['reason'],
|
|
result='blocked',
|
|
execution_plan={'plan': steps, 'policyReason': policy['reason']},
|
|
redacted_metadata=metadata,
|
|
)
|
|
return {'success': False, 'decision': 'blocked', 'status': 'failed', 'policy_decision': 'blocked', 'result': 'blocked', 'message': policy['reason'], 'auditId': audit.id}
|
|
|
|
if policy['decision'] == 'approval_required':
|
|
approval = create_approval(session, action=action, target=target, source=source, risk=risk, reason=policy['reason'], requester=source)
|
|
audit = log_event(
|
|
session,
|
|
source='action',
|
|
identity=source,
|
|
action_id=action,
|
|
target=target,
|
|
risk=risk,
|
|
policy_decision='approval_required',
|
|
reason=policy['reason'],
|
|
result='approval_requested',
|
|
execution_plan={'plan': steps, 'approvalId': approval.id},
|
|
redacted_metadata={**metadata, 'approval_id': approval.id},
|
|
)
|
|
return {'success': False, 'decision': 'approval_required', 'status': 'failed', 'policy_decision': 'approval_required', 'result': 'approval_required', 'message': 'Action requires approval before execution.', 'approvalId': approval.id, 'auditId': audit.id}
|
|
|
|
job = create_job(session, action_id=action, target=target, source=source, risk=risk, policy_decision='allowed', steps=steps)
|
|
start_job(session, job)
|
|
set_job_progress(session, job, 35, 'Policy allowed; calling Deluge connector.')
|
|
|
|
connector_result = connector_call(DelugeConnector())
|
|
success = bool(connector_result.get('success'))
|
|
message = str(connector_result.get('message', default_success_message if success else 'deluge_failed'))
|
|
completed = complete_job(session, job, status='succeeded' if success else 'failed', result_summary=message)
|
|
audit = log_event(
|
|
session,
|
|
source='action',
|
|
identity=source,
|
|
action_id=action,
|
|
target=target,
|
|
risk=risk,
|
|
policy_decision='allowed',
|
|
reason=policy['reason'],
|
|
result='success' if success else 'failed',
|
|
execution_plan={'plan': steps, 'jobId': completed.id},
|
|
after_state={'status': connector_result.get('status'), 'message': message},
|
|
redacted_metadata={**metadata, 'connector': 'deluge'},
|
|
)
|
|
|
|
if not success:
|
|
return {'success': False, 'decision': 'failed', 'status': 'failed', 'policy_decision': 'allowed', 'result': 'failed', 'message': message, 'jobId': completed.id, 'auditId': audit.id, 'data': connector_result.get('data')}
|
|
|
|
return {'success': True, 'decision': 'allowed', 'status': 'success', 'policy_decision': 'allowed', 'result': 'success', 'message': message, 'jobId': completed.id, 'auditId': audit.id, 'data': connector_result.get('data', {})}
|