Skip to content

Commit bd24d7e

Browse files
committed
feat: add download locking and flood wait handling in TelegramAdapter
1 parent 93d5e5e commit bd24d7e

1 file changed

Lines changed: 91 additions & 39 deletions

File tree

domain/adapters/providers/telegram.py

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import struct
77
import time
88
from models import StorageAdapter
9-
from telethon import TelegramClient, utils
9+
from telethon import TelegramClient, errors, utils
1010
from telethon.crypto import AuthKey
1111
from telethon.sessions import StringSession
1212
from telethon.tl import types
@@ -90,6 +90,7 @@ def __init__(self, record: StorageAdapter):
9090

9191
self._client: TelegramClient | None = None
9292
self._client_lock = asyncio.Lock()
93+
self._download_lock = asyncio.Lock()
9394
self._message_cache: Dict[int, Tuple[float, object]] = {}
9495

9596
@staticmethod
@@ -229,6 +230,19 @@ async def _get_cached_message(self, message_id: int):
229230
def _get_message_media(message):
230231
return message.document or message.video or message.photo
231232

233+
@staticmethod
234+
def _flood_wait_http_exception(exc: errors.FloodWaitError):
235+
from fastapi import HTTPException
236+
237+
seconds = int(getattr(exc, "seconds", 0) or 0)
238+
if seconds > 0:
239+
return HTTPException(
240+
status_code=429,
241+
detail=f"Telegram 请求过于频繁,请等待 {seconds} 秒后重试",
242+
headers={"Retry-After": str(seconds)},
243+
)
244+
return HTTPException(status_code=429, detail="Telegram 请求过于频繁,请稍后重试")
245+
232246
@staticmethod
233247
def _get_message_file_size(message, media) -> int:
234248
file_meta = message.file
@@ -310,7 +324,7 @@ async def list_dir(self, root: str, rel: str, page_num: int = 1, page_size: int
310324
"size": size,
311325
"mtime": int(message.date.timestamp()),
312326
"type": "file",
313-
"has_thumbnail": self._message_has_thumbnail(message),
327+
"has_thumbnail": False,
314328
})
315329
finally:
316330
if client.is_connected():
@@ -349,8 +363,13 @@ async def read_file(self, root: str, rel: str) -> bytes:
349363
if not message or not self._get_message_media(message):
350364
raise FileNotFoundError(f"在频道 {self.chat_id} 中未找到消息ID为 {message_id} 的文件")
351365

352-
file_bytes = await client.download_media(message, file=bytes)
353-
return file_bytes
366+
try:
367+
async with self._download_lock:
368+
file_bytes = await client.download_media(message, file=bytes)
369+
return file_bytes
370+
except errors.FloodWaitError as exc:
371+
await self._disconnect_shared_client()
372+
raise self._flood_wait_http_exception(exc)
354373

355374
async def read_file_range(self, root: str, rel: str, start: int, end: Optional[int] = None) -> bytes:
356375
from fastapi import HTTPException
@@ -379,22 +398,27 @@ async def read_file_range(self, root: str, rel: str, start: int, end: Optional[i
379398

380399
limit = end - start + 1
381400
data = bytearray()
382-
async for chunk in client.iter_download(
383-
media,
384-
offset=start,
385-
request_size=self._download_chunk_size,
386-
chunk_size=self._download_chunk_size,
387-
file_size=file_size or None,
388-
):
389-
if not chunk:
390-
continue
391-
need = limit - len(data)
392-
if need <= 0:
393-
break
394-
data.extend(chunk[:need])
395-
if len(data) >= limit:
396-
break
397-
return bytes(data)
401+
try:
402+
async with self._download_lock:
403+
async for chunk in client.iter_download(
404+
media,
405+
offset=start,
406+
request_size=self._download_chunk_size,
407+
chunk_size=self._download_chunk_size,
408+
file_size=file_size or None,
409+
):
410+
if not chunk:
411+
continue
412+
need = limit - len(data)
413+
if need <= 0:
414+
break
415+
data.extend(chunk[:need])
416+
if len(data) >= limit:
417+
break
418+
return bytes(data)
419+
except errors.FloodWaitError as exc:
420+
await self._disconnect_shared_client()
421+
raise self._flood_wait_http_exception(exc)
398422

399423
async def write_file(self, root: str, rel: str, data: bytes):
400424
"""将字节数据作为文件上传"""
@@ -515,7 +539,8 @@ async def get_thumbnail(self, root: str, rel: str, size: str = "medium"):
515539
if embedded and isinstance(thumb, types.PhotoStrippedSize):
516540
return utils.stripped_photo_to_jpg(bytes(embedded))
517541

518-
result = await client.download_media(message, bytes, thumb=thumb)
542+
async with self._download_lock:
543+
result = await client.download_media(message, bytes, thumb=thumb)
519544
if isinstance(result, (bytes, bytearray)):
520545
return bytes(result)
521546
return None
@@ -602,29 +627,56 @@ async def stream_file(self, root: str, rel: str, range_header: str | None):
602627
headers["Content-Length"] = str(end - start + 1)
603628

604629
async def iterator():
630+
downloaded = 0
605631
try:
606632
limit = end - start + 1
607-
downloaded = 0
608-
609-
async for chunk in client.iter_download(
610-
media,
611-
offset=start,
612-
request_size=self._download_chunk_size,
613-
chunk_size=self._download_chunk_size,
614-
file_size=file_size,
615-
):
616-
if downloaded + len(chunk) > limit:
617-
yield chunk[:limit - downloaded]
618-
break
619-
yield chunk
620-
downloaded += len(chunk)
621-
if downloaded >= limit:
622-
break
633+
async with self._download_lock:
634+
async for chunk in client.iter_download(
635+
media,
636+
offset=start,
637+
request_size=self._download_chunk_size,
638+
chunk_size=self._download_chunk_size,
639+
file_size=file_size,
640+
):
641+
if not chunk:
642+
continue
643+
remaining = limit - downloaded
644+
if remaining <= 0:
645+
break
646+
data = chunk[:remaining]
647+
downloaded += len(data)
648+
yield data
649+
if downloaded >= limit:
650+
break
651+
except errors.FloodWaitError as exc:
652+
await self._disconnect_shared_client()
653+
if downloaded == 0:
654+
raise self._flood_wait_http_exception(exc)
655+
seconds = int(getattr(exc, "seconds", 0) or 0)
656+
print(f"Telegram streaming stopped by FloodWait after partial response, wait={seconds}s")
657+
return
623658
except Exception:
624659
await self._disconnect_shared_client()
625660
raise
626661

627-
return StreamingResponse(iterator(), status_code=status, headers=headers)
662+
agen = iterator()
663+
try:
664+
first_chunk = await agen.__anext__()
665+
except StopAsyncIteration:
666+
first_chunk = b""
667+
except HTTPException:
668+
raise
669+
670+
async def response_iterator():
671+
try:
672+
if first_chunk:
673+
yield first_chunk
674+
async for chunk in agen:
675+
yield chunk
676+
finally:
677+
await agen.aclose()
678+
679+
return StreamingResponse(response_iterator(), status_code=status, headers=headers)
628680

629681
except HTTPException:
630682
raise
@@ -654,7 +706,7 @@ async def stat_file(self, root: str, rel: str):
654706
"size": size,
655707
"mtime": int(message.date.timestamp()),
656708
"type": "file",
657-
"has_thumbnail": self._message_has_thumbnail(message),
709+
"has_thumbnail": False,
658710
}
659711

660712
def ADAPTER_FACTORY(rec: StorageAdapter) -> TelegramAdapter:

0 commit comments

Comments
 (0)