feat: add download locking and flood wait handling in TelegramAdapter

This commit is contained in:
shiyu
2026-05-06 23:00:10 +08:00
parent 93d5e5e313
commit bd24d7eeeb
+91 -39
View File
@@ -6,7 +6,7 @@ import os
import struct import struct
import time import time
from models import StorageAdapter from models import StorageAdapter
from telethon import TelegramClient, utils from telethon import TelegramClient, errors, utils
from telethon.crypto import AuthKey from telethon.crypto import AuthKey
from telethon.sessions import StringSession from telethon.sessions import StringSession
from telethon.tl import types from telethon.tl import types
@@ -90,6 +90,7 @@ class TelegramAdapter:
self._client: TelegramClient | None = None self._client: TelegramClient | None = None
self._client_lock = asyncio.Lock() self._client_lock = asyncio.Lock()
self._download_lock = asyncio.Lock()
self._message_cache: Dict[int, Tuple[float, object]] = {} self._message_cache: Dict[int, Tuple[float, object]] = {}
@staticmethod @staticmethod
@@ -229,6 +230,19 @@ class TelegramAdapter:
def _get_message_media(message): def _get_message_media(message):
return message.document or message.video or message.photo return message.document or message.video or message.photo
@staticmethod
def _flood_wait_http_exception(exc: errors.FloodWaitError):
from fastapi import HTTPException
seconds = int(getattr(exc, "seconds", 0) or 0)
if seconds > 0:
return HTTPException(
status_code=429,
detail=f"Telegram 请求过于频繁,请等待 {seconds} 秒后重试",
headers={"Retry-After": str(seconds)},
)
return HTTPException(status_code=429, detail="Telegram 请求过于频繁,请稍后重试")
@staticmethod @staticmethod
def _get_message_file_size(message, media) -> int: def _get_message_file_size(message, media) -> int:
file_meta = message.file file_meta = message.file
@@ -310,7 +324,7 @@ class TelegramAdapter:
"size": size, "size": size,
"mtime": int(message.date.timestamp()), "mtime": int(message.date.timestamp()),
"type": "file", "type": "file",
"has_thumbnail": self._message_has_thumbnail(message), "has_thumbnail": False,
}) })
finally: finally:
if client.is_connected(): if client.is_connected():
@@ -349,8 +363,13 @@ class TelegramAdapter:
if not message or not self._get_message_media(message): if not message or not self._get_message_media(message):
raise FileNotFoundError(f"在频道 {self.chat_id} 中未找到消息ID为 {message_id} 的文件") raise FileNotFoundError(f"在频道 {self.chat_id} 中未找到消息ID为 {message_id} 的文件")
file_bytes = await client.download_media(message, file=bytes) try:
return file_bytes async with self._download_lock:
file_bytes = await client.download_media(message, file=bytes)
return file_bytes
except errors.FloodWaitError as exc:
await self._disconnect_shared_client()
raise self._flood_wait_http_exception(exc)
async def read_file_range(self, root: str, rel: str, start: int, end: Optional[int] = None) -> bytes: async def read_file_range(self, root: str, rel: str, start: int, end: Optional[int] = None) -> bytes:
from fastapi import HTTPException from fastapi import HTTPException
@@ -379,22 +398,27 @@ class TelegramAdapter:
limit = end - start + 1 limit = end - start + 1
data = bytearray() data = bytearray()
async for chunk in client.iter_download( try:
media, async with self._download_lock:
offset=start, async for chunk in client.iter_download(
request_size=self._download_chunk_size, media,
chunk_size=self._download_chunk_size, offset=start,
file_size=file_size or None, request_size=self._download_chunk_size,
): chunk_size=self._download_chunk_size,
if not chunk: file_size=file_size or None,
continue ):
need = limit - len(data) if not chunk:
if need <= 0: continue
break need = limit - len(data)
data.extend(chunk[:need]) if need <= 0:
if len(data) >= limit: break
break data.extend(chunk[:need])
return bytes(data) if len(data) >= limit:
break
return bytes(data)
except errors.FloodWaitError as exc:
await self._disconnect_shared_client()
raise self._flood_wait_http_exception(exc)
async def write_file(self, root: str, rel: str, data: bytes): async def write_file(self, root: str, rel: str, data: bytes):
"""将字节数据作为文件上传""" """将字节数据作为文件上传"""
@@ -515,7 +539,8 @@ class TelegramAdapter:
if embedded and isinstance(thumb, types.PhotoStrippedSize): if embedded and isinstance(thumb, types.PhotoStrippedSize):
return utils.stripped_photo_to_jpg(bytes(embedded)) return utils.stripped_photo_to_jpg(bytes(embedded))
result = await client.download_media(message, bytes, thumb=thumb) async with self._download_lock:
result = await client.download_media(message, bytes, thumb=thumb)
if isinstance(result, (bytes, bytearray)): if isinstance(result, (bytes, bytearray)):
return bytes(result) return bytes(result)
return None return None
@@ -602,29 +627,56 @@ class TelegramAdapter:
headers["Content-Length"] = str(end - start + 1) headers["Content-Length"] = str(end - start + 1)
async def iterator(): async def iterator():
downloaded = 0
try: try:
limit = end - start + 1 limit = end - start + 1
downloaded = 0 async with self._download_lock:
async for chunk in client.iter_download(
async for chunk in client.iter_download( media,
media, offset=start,
offset=start, request_size=self._download_chunk_size,
request_size=self._download_chunk_size, chunk_size=self._download_chunk_size,
chunk_size=self._download_chunk_size, file_size=file_size,
file_size=file_size, ):
): if not chunk:
if downloaded + len(chunk) > limit: continue
yield chunk[:limit - downloaded] remaining = limit - downloaded
break if remaining <= 0:
yield chunk break
downloaded += len(chunk) data = chunk[:remaining]
if downloaded >= limit: downloaded += len(data)
break yield data
if downloaded >= limit:
break
except errors.FloodWaitError as exc:
await self._disconnect_shared_client()
if downloaded == 0:
raise self._flood_wait_http_exception(exc)
seconds = int(getattr(exc, "seconds", 0) or 0)
print(f"Telegram streaming stopped by FloodWait after partial response, wait={seconds}s")
return
except Exception: except Exception:
await self._disconnect_shared_client() await self._disconnect_shared_client()
raise raise
return StreamingResponse(iterator(), status_code=status, headers=headers) agen = iterator()
try:
first_chunk = await agen.__anext__()
except StopAsyncIteration:
first_chunk = b""
except HTTPException:
raise
async def response_iterator():
try:
if first_chunk:
yield first_chunk
async for chunk in agen:
yield chunk
finally:
await agen.aclose()
return StreamingResponse(response_iterator(), status_code=status, headers=headers)
except HTTPException: except HTTPException:
raise raise
@@ -654,7 +706,7 @@ class TelegramAdapter:
"size": size, "size": size,
"mtime": int(message.date.timestamp()), "mtime": int(message.date.timestamp()),
"type": "file", "type": "file",
"has_thumbnail": self._message_has_thumbnail(message), "has_thumbnail": False,
} }
def ADAPTER_FACTORY(rec: StorageAdapter) -> TelegramAdapter: def ADAPTER_FACTORY(rec: StorageAdapter) -> TelegramAdapter: