mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
831 lines
29 KiB
Python
831 lines
29 KiB
Python
"""
|
|
Helper functions for inter-service communication used by the main service.
|
|
|
|
Behavior:
|
|
- In development (single-process) the helpers call the in-process service modules directly.
|
|
- In Docker/production the helpers perform HTTP calls to the configured service URLs.
|
|
"""
|
|
from typing import Optional, Dict, Any
|
|
import os
|
|
import logging
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
from fastapi import HTTPException, status
|
|
|
|
# Import request models for in-process calls
|
|
|
|
logger = logging.getLogger("uvicorn.error")
|
|
|
|
|
|
def _default_file_storage_base_url() -> str:
|
|
lan = os.getenv("LAN_IP", "").strip()
|
|
if lan:
|
|
return f"http://{lan}:8302"
|
|
return "http://127.0.0.1:8302"
|
|
|
|
|
|
def _get_messaging_module():
|
|
try:
|
|
from backend.services.messaging import main as messaging_module
|
|
return messaging_module
|
|
except Exception:
|
|
try:
|
|
from services.messaging import main as messaging_module # type: ignore
|
|
return messaging_module
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _get_file_storage_module():
|
|
try:
|
|
from backend.services.file_storage import main as storage_module
|
|
return storage_module
|
|
except Exception:
|
|
try:
|
|
from services.file_storage import main as storage_module # type: ignore
|
|
return storage_module
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
async def get_messaging_transport_public_key(timeout: float = 5.0) -> Dict[str, Any]:
|
|
"""
|
|
Return messaging service ephemeral transport public key.
|
|
"""
|
|
mod = _get_messaging_module()
|
|
if mod:
|
|
# in-process async call
|
|
try:
|
|
return await mod.get_transport_public_key() # type: ignore
|
|
except Exception as e:
|
|
logger.error("In-process messaging.get_transport_public_key failed: %s", e)
|
|
raise
|
|
|
|
# Out-of-process HTTP
|
|
messaging_url = os.getenv("MESSAGING_SERVICE_URL", "http://messaging:8301")
|
|
url = f"{messaging_url.rstrip('/')}/key/transport/public"
|
|
try:
|
|
try:
|
|
import httpx
|
|
r = httpx.get(url, timeout=timeout)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception:
|
|
from urllib import request
|
|
with request.urlopen(url, timeout=timeout) as r:
|
|
return json.loads(r.read())
|
|
except Exception as e:
|
|
logger.error("Failed to fetch messaging transport public key: %s", e)
|
|
raise
|
|
|
|
|
|
async def get_compliance_public_key(timeout: float = 5.0) -> Dict[str, Any]:
|
|
"""
|
|
Return compliance system public key (for MEK wrapping).
|
|
"""
|
|
try:
|
|
from services.shared.message_retention import get_message_retention
|
|
except ImportError:
|
|
from backend.services.shared.message_retention import get_message_retention # type: ignore
|
|
if get_message_retention().never_store_compliance_mek():
|
|
return {"public_key_b64": ""}
|
|
|
|
mod = _get_messaging_module()
|
|
if mod:
|
|
# in-process async call
|
|
try:
|
|
key = mod.get_compliance_public_key()
|
|
return {"public_key_b64": key}
|
|
except Exception as e:
|
|
logger.error("In-process messaging.get_compliance_public_key failed: %s", e)
|
|
raise
|
|
|
|
# Out-of-process: Compliance key should be configured via environment variable
|
|
# The compliance public key is not exposed via HTTP for security reasons
|
|
compliance_key = os.getenv("COMPLIANCE_PUBLIC_KEY", "").strip()
|
|
if compliance_key:
|
|
return {"public_key_b64": compliance_key}
|
|
|
|
logger.error("COMPLIANCE_PUBLIC_KEY environment variable not set and messaging service not available in-process")
|
|
raise RuntimeError("Compliance public key not available - set COMPLIANCE_PUBLIC_KEY environment variable")
|
|
|
|
|
|
async def invalidate_messaging_key(timeout: float = 5.0) -> Dict[str, Any]:
|
|
"""
|
|
Request messaging service to invalidate its current ephemeral transport key (rotate).
|
|
"""
|
|
mod = _get_messaging_module()
|
|
if mod:
|
|
try:
|
|
return await mod.invalidate_transport_key() # type: ignore
|
|
except Exception as e:
|
|
logger.error("In-process messaging.invalidate_transport_key failed: %s", e)
|
|
raise
|
|
|
|
messaging_url = os.getenv("MESSAGING_SERVICE_URL", "http://messaging:8301")
|
|
url = f"{messaging_url.rstrip('/')}/key/transport/invalidate"
|
|
try:
|
|
try:
|
|
import httpx
|
|
r = httpx.post(url, timeout=timeout)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception:
|
|
from urllib import request
|
|
req = request.Request(url, method="POST")
|
|
with request.urlopen(req, timeout=timeout) as r:
|
|
return json.loads(r.read())
|
|
except Exception as e:
|
|
logger.error("Failed to invalidate messaging key: %s", e)
|
|
raise
|
|
|
|
|
|
async def upload_file_to_storage(file_obj: Any, timeout: float = 30.0) -> Dict[str, Any]:
|
|
"""
|
|
Upload a file to file storage service. Returns JSON response.
|
|
In-process: calls the in-process service.
|
|
Out-of-process: performs HTTP call to configured service URL.
|
|
"""
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
# Call the upload endpoint directly on the in-process module
|
|
return await mod.upload_file(None, file_obj) # type: ignore
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.upload_file failed: %s", e)
|
|
raise
|
|
|
|
# Out-of-process HTTP
|
|
# Prefer explicit FILE_STORAGE_URL, fall back to FILE_STORAGE_SERVICE_URL, default to localhost for dev
|
|
storage_url = os.getenv("FILE_STORAGE_URL") or os.getenv("FILE_STORAGE_SERVICE_URL") or _default_file_storage_base_url()
|
|
url = f"{storage_url.rstrip('/')}/upload"
|
|
try:
|
|
try:
|
|
import httpx
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.post(url, files={"file": file_obj})
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception:
|
|
from urllib import request
|
|
# Synchronous fallback using urllib
|
|
req = request.Request(url, method="POST")
|
|
if hasattr(file_obj, "read"):
|
|
data = file_obj.read()
|
|
else:
|
|
data = file_obj
|
|
req.data = data
|
|
req.add_header("Content-Type", "application/octet-stream")
|
|
with request.urlopen(req, timeout=timeout) as r:
|
|
return json.loads(r.read())
|
|
except Exception as e:
|
|
logger.error("Failed to upload file to storage: %s", e)
|
|
raise
|
|
|
|
|
|
async def store_encrypted_file(
|
|
encrypted_file_data_b64: str,
|
|
filename: str,
|
|
content_type: str = "application/octet-stream",
|
|
sender_id: int = None,
|
|
recipient_id: int = None,
|
|
timeout: float = 30.0,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Store an encrypted file (base64 encoded) in the file storage service.
|
|
|
|
Returns:
|
|
{
|
|
"file_id": stored filename,
|
|
"filename": original filename,
|
|
"size": file size in bytes,
|
|
"path": access path
|
|
}
|
|
"""
|
|
mod = _get_file_storage_module()
|
|
# Build allowed users list once for both in-process and HTTP modes
|
|
allowed_user_ids: list[int] = []
|
|
if sender_id is not None:
|
|
allowed_user_ids.append(sender_id)
|
|
if recipient_id is not None:
|
|
allowed_user_ids.append(recipient_id)
|
|
|
|
if mod:
|
|
try:
|
|
# In-process: call internal function directly
|
|
return await mod.upload_base64_internal(
|
|
filename=filename,
|
|
data_b64=encrypted_file_data_b64,
|
|
content_type=content_type,
|
|
allowed_user_ids=allowed_user_ids,
|
|
)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.store_encrypted_file failed: %s", e)
|
|
raise
|
|
|
|
# Out-of-process HTTP
|
|
# Prefer explicit FILE_STORAGE_URL, fall back to FILE_STORAGE_SERVICE_URL, default to localhost for dev
|
|
file_storage_url = os.getenv("FILE_STORAGE_URL") or os.getenv("FILE_STORAGE_SERVICE_URL") or _default_file_storage_base_url()
|
|
url = f"{file_storage_url.rstrip('/')}/upload-base64"
|
|
try:
|
|
try:
|
|
import httpx
|
|
|
|
payload = {
|
|
"filename": filename,
|
|
"data_b64": encrypted_file_data_b64,
|
|
"content_type": content_type,
|
|
"allowed_user_ids": allowed_user_ids,
|
|
}
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.post(url, json=payload)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception:
|
|
from urllib import request
|
|
|
|
payload = {
|
|
"filename": filename,
|
|
"data_b64": encrypted_file_data_b64,
|
|
"content_type": content_type,
|
|
"allowed_user_ids": allowed_user_ids,
|
|
}
|
|
req = request.Request(url, method="POST")
|
|
req.data = json.dumps(payload).encode("utf-8")
|
|
req.add_header("Content-Type", "application/json")
|
|
with request.urlopen(req, timeout=timeout) as r:
|
|
return json.loads(r.read())
|
|
except Exception as e:
|
|
logger.error("Failed to store encrypted file: %s", e)
|
|
# Fallback: attempt to store the file locally under data/file_storage/files
|
|
try:
|
|
import base64
|
|
from pathlib import Path
|
|
import uuid
|
|
|
|
# Store encrypted files in the same directory the messaging service serves from
|
|
FILES_DIR = Path("data/uploads/files/encrypted")
|
|
FILES_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
decoded = base64.b64decode(encrypted_file_data_b64)
|
|
stored_name = f"{uuid.uuid4().hex}_{filename}"
|
|
dest = FILES_DIR / stored_name
|
|
with open(dest, "wb") as f:
|
|
f.write(decoded)
|
|
try:
|
|
dest.chmod(0o644)
|
|
except Exception:
|
|
logger.debug("Could not chmod fallback file %s", dest)
|
|
|
|
logger.info("FALLBACK: Stored encrypted file locally: %s", dest)
|
|
return {
|
|
"file_id": stored_name,
|
|
"filename": filename,
|
|
"size": len(decoded),
|
|
"path": f"/uploads/files/encrypted/{stored_name}",
|
|
}
|
|
except Exception as e2:
|
|
logger.exception("Fallback local storage failed: %s", e2)
|
|
raise
|
|
|
|
|
|
async def process_message_in_messaging_service(
|
|
client_public_key_b64: str,
|
|
transport_nonce_b64: str,
|
|
transport_ciphertext_b64: str,
|
|
compliance_public_key_b64: str,
|
|
sender_public_key_b64: str,
|
|
recipient_public_key_b64: str,
|
|
timeout: float = 5.0,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Process an encrypted message through the messaging service envelope encryption pipeline.
|
|
|
|
In-process: calls the in-process service.
|
|
Out-of-process: performs HTTP call to configured service URL.
|
|
|
|
Args:
|
|
client_public_key_b64: Client's ephemeral public key
|
|
transport_nonce_b64: Nonce for transport encryption
|
|
transport_ciphertext_b64: Encrypted message
|
|
compliance_public_key_b64: Compliance system public key
|
|
sender_public_key_b64: Sender's public key
|
|
recipient_public_key_b64: Recipient's public key
|
|
timeout: Request timeout in seconds
|
|
|
|
Returns:
|
|
Dict with encrypted message and wrapped MEKs:
|
|
{
|
|
"nonce": base64-encoded nonce,
|
|
"ciphertext": base64-encoded ciphertext,
|
|
"compliance_wrapped_mek": wrapped MEK,
|
|
"sender_wrapped_mek": wrapped MEK,
|
|
"recipient_wrapped_mek": wrapped MEK,
|
|
}
|
|
"""
|
|
mod = _get_messaging_module()
|
|
if mod:
|
|
try:
|
|
# In-process: call the process endpoint directly
|
|
return await mod.process_message(
|
|
client_public_key_b64=client_public_key_b64,
|
|
transport_nonce_b64=transport_nonce_b64,
|
|
transport_ciphertext_b64=transport_ciphertext_b64,
|
|
compliance_public_key_b64=compliance_public_key_b64,
|
|
sender_public_key_b64=sender_public_key_b64,
|
|
recipient_public_key_b64=recipient_public_key_b64,
|
|
) # type: ignore
|
|
except Exception as e:
|
|
logger.error("In-process messaging.process_message failed: %s", e)
|
|
raise
|
|
|
|
# Out-of-process HTTP
|
|
messaging_url = os.getenv("MESSAGING_SERVICE_URL", "http://messaging:8301")
|
|
url = f"{messaging_url.rstrip('/')}/process"
|
|
try:
|
|
try:
|
|
import httpx
|
|
payload = {
|
|
"client_public_key_b64": client_public_key_b64,
|
|
"transport_nonce_b64": transport_nonce_b64,
|
|
"transport_ciphertext_b64": transport_ciphertext_b64,
|
|
"compliance_public_key_b64": compliance_public_key_b64,
|
|
"sender_public_key_b64": sender_public_key_b64,
|
|
"recipient_public_key_b64": recipient_public_key_b64,
|
|
}
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.post(url, json=payload)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception:
|
|
from urllib import request
|
|
payload = {
|
|
"client_public_key_b64": client_public_key_b64,
|
|
"transport_nonce_b64": transport_nonce_b64,
|
|
"transport_ciphertext_b64": transport_ciphertext_b64,
|
|
"compliance_public_key_b64": compliance_public_key_b64,
|
|
"sender_public_key_b64": sender_public_key_b64,
|
|
"recipient_public_key_b64": recipient_public_key_b64,
|
|
}
|
|
req = request.Request(url, method="POST")
|
|
req.data = json.dumps(payload).encode("utf-8")
|
|
req.add_header("Content-Type", "application/json")
|
|
with request.urlopen(req, timeout=timeout) as r:
|
|
return json.loads(r.read())
|
|
except Exception as e:
|
|
logger.error("Failed to process message in messaging service: %s", e)
|
|
raise
|
|
|
|
|
|
async def process_message_with_files_in_messaging_service(
|
|
client_public_key_b64: str,
|
|
transport_nonce_b64: str,
|
|
transport_ciphertext_b64: str,
|
|
compliance_public_key_b64: str,
|
|
sender_public_key_b64: str,
|
|
recipient_public_key_b64: str,
|
|
transport_files: list[dict[str, str]],
|
|
timeout: float = 60.0,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Process an encrypted message and transport-encrypted files using a single MEK.
|
|
|
|
Returns:
|
|
{
|
|
"message": {"nonce": str, "ciphertext": str},
|
|
"files": [{"nonce": str, "ciphertext": str}, ...],
|
|
"compliance_wrapped_mek": str,
|
|
"sender_wrapped_mek": str,
|
|
"recipient_wrapped_mek": str,
|
|
}
|
|
"""
|
|
mod = _get_messaging_module()
|
|
if mod:
|
|
try:
|
|
return await mod.process_message_with_files( # type: ignore
|
|
client_public_key_b64=client_public_key_b64,
|
|
transport_nonce_b64=transport_nonce_b64,
|
|
transport_ciphertext_b64=transport_ciphertext_b64,
|
|
compliance_public_key_b64=compliance_public_key_b64,
|
|
sender_public_key_b64=sender_public_key_b64,
|
|
recipient_public_key_b64=recipient_public_key_b64,
|
|
transport_files=transport_files,
|
|
)
|
|
except Exception as e:
|
|
logger.error("In-process messaging.process_message_with_files failed: %s", e)
|
|
raise
|
|
|
|
messaging_url = os.getenv("MESSAGING_SERVICE_URL", "http://messaging:8301")
|
|
url = f"{messaging_url.rstrip('/')}/process-with-files"
|
|
payload = {
|
|
"client_public_key_b64": client_public_key_b64,
|
|
"transport_nonce_b64": transport_nonce_b64,
|
|
"transport_ciphertext_b64": transport_ciphertext_b64,
|
|
"compliance_public_key_b64": compliance_public_key_b64,
|
|
"sender_public_key_b64": sender_public_key_b64,
|
|
"recipient_public_key_b64": recipient_public_key_b64,
|
|
"files": transport_files,
|
|
}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.post(url, json=payload)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except httpx.HTTPStatusError as e:
|
|
if e.response.status_code == status.HTTP_400_BAD_REQUEST:
|
|
try:
|
|
body = e.response.json()
|
|
detail = body.get("detail", str(body)) if isinstance(body, dict) else str(body)
|
|
except Exception:
|
|
detail = (e.response.text or "").strip() or str(e)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=detail,
|
|
) from e
|
|
logger.error("Failed to process message+files in messaging service: %s", e)
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Failed to process message+files in messaging service: %s", e)
|
|
raise
|
|
|
|
|
|
async def init_resumable_upload_in_storage(
|
|
filename: str,
|
|
total_size: int,
|
|
allowed_user_ids: list[int],
|
|
chunk_size: int | None = None,
|
|
timeout: float = 10.0,
|
|
) -> Dict[str, Any]:
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return await mod.init_resumable_upload_internal(
|
|
filename=filename,
|
|
total_size=total_size,
|
|
allowed_user_ids=allowed_user_ids,
|
|
chunk_size=chunk_size,
|
|
)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.init_resumable_upload failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = os.getenv("FILE_STORAGE_URL") or os.getenv("FILE_STORAGE_SERVICE_URL") or _default_file_storage_base_url()
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/resumable/init"
|
|
payload = {
|
|
"filename": filename,
|
|
"total_size": total_size,
|
|
"allowed_user_ids": allowed_user_ids,
|
|
}
|
|
if chunk_size is not None:
|
|
payload["chunk_size"] = chunk_size
|
|
|
|
import httpx
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.post(url, json=payload)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def get_resumable_upload_status_in_storage(
|
|
upload_id: str,
|
|
user_id: int,
|
|
timeout: float = 10.0,
|
|
) -> Dict[str, Any]:
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return await mod.get_resumable_upload_status_internal(upload_id, user_id)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.get_resumable_upload_status failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = os.getenv("FILE_STORAGE_URL") or os.getenv("FILE_STORAGE_SERVICE_URL") or _default_file_storage_base_url()
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/resumable/{upload_id}"
|
|
|
|
import httpx
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.get(url, headers={"X-User-ID": str(user_id)})
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def upload_resumable_chunk_in_storage(
|
|
upload_id: str,
|
|
user_id: int,
|
|
offset: int,
|
|
data_b64: str,
|
|
timeout: float = 30.0,
|
|
) -> Dict[str, Any]:
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return await mod.upload_resumable_chunk_internal(
|
|
upload_id, user_id, offset, data_b64
|
|
)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.upload_resumable_chunk failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = os.getenv("FILE_STORAGE_URL") or os.getenv("FILE_STORAGE_SERVICE_URL") or _default_file_storage_base_url()
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/resumable/{upload_id}"
|
|
payload = {
|
|
"offset": offset,
|
|
"data_b64": data_b64,
|
|
}
|
|
|
|
import httpx
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.patch(url, json=payload, headers={"X-User-ID": str(user_id)})
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def complete_resumable_upload_in_storage(
|
|
upload_id: str,
|
|
user_id: int,
|
|
timeout: float = 10.0,
|
|
) -> Dict[str, Any]:
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return await mod.complete_resumable_upload_internal(upload_id, user_id)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.complete_resumable_upload failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = os.getenv("FILE_STORAGE_URL") or os.getenv("FILE_STORAGE_SERVICE_URL") or _default_file_storage_base_url()
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/resumable/{upload_id}/complete"
|
|
|
|
import httpx
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.post(url, json={"upload_id": upload_id}, headers={"X-User-ID": str(user_id)})
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def get_resumable_upload_blob_path_in_storage(
|
|
upload_id: str,
|
|
user_id: int,
|
|
timeout: float = 30.0,
|
|
) -> Dict[str, Any]:
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return await mod.get_resumable_upload_blob_path_internal(upload_id, user_id)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.get_resumable_upload_blob_path failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = os.getenv("FILE_STORAGE_URL") or os.getenv("FILE_STORAGE_SERVICE_URL") or _default_file_storage_base_url()
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/resumable/{upload_id}/blob-path"
|
|
|
|
import httpx
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.get(url, headers={"X-User-ID": str(user_id)})
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def store_encrypted_file_from_path(
|
|
source_path: str,
|
|
filename: str,
|
|
content_type: str = "application/octet-stream",
|
|
sender_id: int = None,
|
|
recipient_id: int = None,
|
|
timeout: float = 120.0,
|
|
) -> Dict[str, Any]:
|
|
mod = _get_file_storage_module()
|
|
allowed_user_ids: list[int] = []
|
|
if sender_id is not None:
|
|
allowed_user_ids.append(sender_id)
|
|
if recipient_id is not None:
|
|
allowed_user_ids.append(recipient_id)
|
|
|
|
if mod:
|
|
from pathlib import Path
|
|
|
|
return await mod.upload_encrypted_file_from_path_internal(
|
|
filename=filename,
|
|
source_path=Path(source_path),
|
|
content_type=content_type,
|
|
allowed_user_ids=allowed_user_ids,
|
|
)
|
|
|
|
raise RuntimeError("store_encrypted_file_from_path requires in-process file_storage")
|
|
|
|
|
|
async def get_resumable_upload_data_in_storage(
|
|
upload_id: str,
|
|
user_id: int,
|
|
timeout: float = 30.0,
|
|
) -> Dict[str, Any]:
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return await mod.get_resumable_upload_data_internal(upload_id, user_id)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.get_resumable_upload_data failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = os.getenv("FILE_STORAGE_URL") or os.getenv("FILE_STORAGE_SERVICE_URL") or _default_file_storage_base_url()
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/resumable/{upload_id}/data-b64"
|
|
|
|
import httpx
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.get(url, headers={"X-User-ID": str(user_id)})
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def store_normal_file_from_path_in_storage(
|
|
stored_name: str,
|
|
source_path: str | Path,
|
|
timeout: float = 120.0,
|
|
) -> Dict[str, Any]:
|
|
"""Persist a plain public-chat attachment where file downloads are served from."""
|
|
mod = _get_file_storage_module()
|
|
src = Path(source_path)
|
|
if mod:
|
|
try:
|
|
return await mod.store_normal_file_from_path_internal(stored_name, src)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.store_normal_file_from_path failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = (
|
|
os.getenv("FILE_STORAGE_URL")
|
|
or os.getenv("FILE_STORAGE_SERVICE_URL")
|
|
or _default_file_storage_base_url()
|
|
)
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/files/normal/store"
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
with open(src, "rb") as file_handle:
|
|
r = await client.post(
|
|
url,
|
|
data={"stored_name": stored_name},
|
|
files={"file": (Path(stored_name).name, file_handle, "application/octet-stream")},
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def store_public_thumb_in_storage(
|
|
stored_name: str,
|
|
jpeg_bytes: bytes,
|
|
*,
|
|
width: int,
|
|
height: int,
|
|
file_size: int,
|
|
timeout: float = 30.0,
|
|
) -> Dict[str, Any]:
|
|
"""Persist a public-chat thumbnail under file_storage THUMBS_DIR."""
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return await mod.store_public_thumb_internal(
|
|
stored_name,
|
|
jpeg_bytes,
|
|
width=width,
|
|
height=height,
|
|
file_size=file_size,
|
|
)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.store_public_thumb failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = (
|
|
os.getenv("FILE_STORAGE_URL")
|
|
or os.getenv("FILE_STORAGE_SERVICE_URL")
|
|
or _default_file_storage_base_url()
|
|
)
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/files/thumbs/store"
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.post(
|
|
url,
|
|
data={
|
|
"stored_name": stored_name,
|
|
"width": str(width),
|
|
"height": str(height),
|
|
"file_size": str(file_size),
|
|
},
|
|
files={"file": (f"{Path(stored_name).stem}.jpg", jpeg_bytes, "image/jpeg")},
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def store_public_image_dimensions_in_storage(
|
|
stored_name: str,
|
|
*,
|
|
width: int,
|
|
height: int,
|
|
file_size: int,
|
|
timeout: float = 30.0,
|
|
) -> Dict[str, Any]:
|
|
"""Persist image dimensions for large public attachments (no JPEG thumbnail)."""
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return await mod.store_public_image_dimensions_internal(
|
|
stored_name,
|
|
width=width,
|
|
height=height,
|
|
file_size=file_size,
|
|
)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.store_public_image_dimensions failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = (
|
|
os.getenv("FILE_STORAGE_URL")
|
|
or os.getenv("FILE_STORAGE_SERVICE_URL")
|
|
or _default_file_storage_base_url()
|
|
)
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/files/thumbs/dimensions"
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.post(
|
|
url,
|
|
data={
|
|
"stored_name": stored_name,
|
|
"width": str(width),
|
|
"height": str(height),
|
|
"file_size": str(file_size),
|
|
},
|
|
)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
async def get_public_thumb_meta_in_storage(
|
|
stored_name: str,
|
|
timeout: float = 10.0,
|
|
) -> Dict[str, Any] | None:
|
|
"""Load thumbnail base64 + dimensions for a normal attachment basename."""
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return mod.get_public_thumb_meta_internal(stored_name)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.get_public_thumb_meta failed: %s", e)
|
|
return None
|
|
|
|
file_storage_url = (
|
|
os.getenv("FILE_STORAGE_URL")
|
|
or os.getenv("FILE_STORAGE_SERVICE_URL")
|
|
or _default_file_storage_base_url()
|
|
)
|
|
stem = Path(stored_name).stem
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/files/thumbs/{stem}.jpg"
|
|
import base64
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.get(url)
|
|
if r.status_code != 200:
|
|
return None
|
|
return {
|
|
"stored_name": Path(stored_name).name,
|
|
"width": 1,
|
|
"height": 1,
|
|
"file_size": 0,
|
|
"thumbnail_b64": base64.b64encode(r.content).decode("ascii"),
|
|
"thumb_path": f"/uploads/files/thumbs/{stem}.jpg",
|
|
}
|
|
except Exception as e:
|
|
logger.error("Remote file_storage.get_public_thumb_meta failed: %s", e)
|
|
return None
|
|
|
|
|
|
async def delete_resumable_upload_in_storage(
|
|
upload_id: str,
|
|
user_id: int,
|
|
timeout: float = 10.0,
|
|
) -> Dict[str, Any]:
|
|
mod = _get_file_storage_module()
|
|
if mod:
|
|
try:
|
|
return await mod.delete_resumable_upload_internal(upload_id, user_id)
|
|
except Exception as e:
|
|
logger.error("In-process file_storage.delete_resumable_upload failed: %s", e)
|
|
raise
|
|
|
|
file_storage_url = os.getenv("FILE_STORAGE_URL") or os.getenv("FILE_STORAGE_SERVICE_URL") or _default_file_storage_base_url()
|
|
url = f"{file_storage_url.rstrip('/')}/uploads/resumable/{upload_id}"
|
|
|
|
import httpx
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
r = await client.delete(url, headers={"X-User-ID": str(user_id)})
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|