Make API encode/decode endpoints async with thread pool

- Added run_in_thread() helper using asyncio.to_thread()
- /encode, /encode/file, /decode use thread pool for CPU-bound ops
- /encode/multipart, /decode/multipart also updated
- Server can now handle concurrent requests without blocking
- Updated version header to v4.2.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-09 22:13:57 -05:00
parent 55d54717f8
commit ea57bdf302

View File

@@ -1,10 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Stegasoo REST API (v4.0.0) Stegasoo REST API (v4.2.0)
FastAPI-based REST API for steganography operations. FastAPI-based REST API for steganography operations.
Supports both text messages and file embedding. Supports both text messages and file embedding.
CHANGES in v4.2.0:
- Async encode/decode operations (run in thread pool)
- Server can handle concurrent requests without blocking
CHANGES in v4.0.0: CHANGES in v4.0.0:
- Added channel key support for deployment/group isolation - Added channel key support for deployment/group isolation
- New /channel endpoints for key management - New /channel endpoints for key management
@@ -21,8 +25,10 @@ NEW in v3.0: LSB and DCT embedding modes.
NEW in v3.0.1: DCT color mode and JPEG output format. NEW in v3.0.1: DCT color mode and JPEG output format.
""" """
import asyncio
import base64 import base64
import sys import sys
from functools import partial
from pathlib import Path from pathlib import Path
from typing import Literal from typing import Literal
@@ -436,6 +442,27 @@ def _get_channel_info(channel_key: str | None) -> tuple[str, str | None]:
return info["mode"], info.get("fingerprint") return info["mode"], info.get("fingerprint")
# ============================================================================
# HELPER: ASYNC EXECUTION
# ============================================================================
async def run_in_thread(func, *args, **kwargs):
"""
Run a CPU-bound function in a thread pool.
This allows the FastAPI server to handle other requests while
encode/decode operations are running. Essential for Pi deployments
where operations can take several seconds.
Usage:
result = await run_in_thread(encode, message=msg, carrier_image=carrier, ...)
"""
if kwargs:
func = partial(func, **kwargs)
return await asyncio.to_thread(func, *args)
# ============================================================================ # ============================================================================
# ROUTES - STATUS & INFO # ROUTES - STATUS & INFO
# ============================================================================ # ============================================================================
@@ -874,8 +901,9 @@ async def api_encode(request: EncodeRequest):
request.embed_mode, request.dct_output_format, request.dct_color_mode request.embed_mode, request.dct_output_format, request.dct_color_mode
) )
# v4.0.0: Include channel_key # v4.2.0: Run CPU-bound encode in thread pool
result = encode( result = await run_in_thread(
encode,
message=request.message, message=request.message,
reference_photo=ref_photo, reference_photo=ref_photo,
carrier_image=carrier, carrier_image=carrier,
@@ -950,8 +978,9 @@ async def api_encode_file(request: EncodeFileRequest):
request.embed_mode, request.dct_output_format, request.dct_color_mode request.embed_mode, request.dct_output_format, request.dct_color_mode
) )
# v4.0.0: Include channel_key # v4.2.0: Run CPU-bound encode in thread pool
result = encode( result = await run_in_thread(
encode,
message=payload, message=payload,
reference_photo=ref_photo, reference_photo=ref_photo,
carrier_image=carrier, carrier_image=carrier,
@@ -1021,8 +1050,9 @@ async def api_decode(request: DecodeRequest):
ref_photo = base64.b64decode(request.reference_photo_base64) ref_photo = base64.b64decode(request.reference_photo_base64)
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
# v4.0.0: Include channel_key # v4.2.0: Run CPU-bound decode in thread pool
result = decode( result = await run_in_thread(
decode,
stego_image=stego, stego_image=stego,
reference_photo=ref_photo, reference_photo=ref_photo,
passphrase=request.passphrase, passphrase=request.passphrase,
@@ -1150,8 +1180,9 @@ async def api_encode_multipart(
# Get DCT parameters # Get DCT parameters
dct_params = _get_dct_params(embed_mode, dct_output_format, dct_color_mode) dct_params = _get_dct_params(embed_mode, dct_output_format, dct_color_mode)
# v4.0.0: Include channel_key # v4.2.0: Run CPU-bound encode in thread pool
result = encode( result = await run_in_thread(
encode,
message=payload, message=payload,
reference_photo=ref_data, reference_photo=ref_data,
carrier_image=carrier_data, carrier_image=carrier_data,
@@ -1264,8 +1295,9 @@ async def api_decode_multipart(
# QR code keys are never password-protected # QR code keys are never password-protected
effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None) effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# v4.0.0: Include channel_key # v4.2.0: Run CPU-bound decode in thread pool
result = decode( result = await run_in_thread(
decode,
stego_image=stego_data, stego_image=stego_data,
reference_photo=ref_data, reference_photo=ref_data,
passphrase=passphrase, passphrase=passphrase,