2626import shutil
2727import sys
2828import time
29+ import math
30+ import tempfile
2931from concurrent .futures .thread import ThreadPoolExecutor
3032from datetime import datetime , timedelta
3133from hashlib import sha256
@@ -198,6 +200,10 @@ class Client(Methods):
198200 Defaults to False, because ``getpass`` (the library used) is known to be problematic in some
199201 terminal environments.
200202
203+ crypto_executor_workers (``int``, *optional*):
204+ Set the maximum crypto executor workers value for a session.
205+ Defaults to ``workers if workers < 5 else 5``.
206+
201207 max_concurrent_transmissions (``int``, *optional*):
202208 Set the maximum amount of concurrent transmissions (uploads & downloads).
203209 A value that is too high may result in network related issues.
@@ -260,6 +266,7 @@ class Client(Methods):
260266 UPGRADED_GIFT_RE = re .compile (r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/(?:nft/|\+))([\w-]+)$" )
261267 CHANNEL_MESSAGE_LINK_RE = re .compile (r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/(?:c/)?)([\w]+)(?:.+)?$" )
262268 WORKERS = min (32 , (os .cpu_count () or 0 ) + 4 ) # os.cpu_count() can be None
269+ CRYPTO_EXECUTOR_WORKERS = WORKERS if WORKERS < 5 else 5
263270 WORKDIR = PARENT_DIR
264271
265272 # Interval of seconds in which the updates watchdog will kick in
@@ -301,6 +308,7 @@ def __init__(
301308 takeout : Optional [bool ] = None ,
302309 sleep_threshold : int = Session .SLEEP_THRESHOLD ,
303310 hide_password : Optional [bool ] = False ,
311+ crypto_executor_workers : int = CRYPTO_EXECUTOR_WORKERS ,
304312 max_concurrent_transmissions : int = MAX_CONCURRENT_TRANSMISSIONS ,
305313 max_message_cache_size : int = MAX_MESSAGE_CACHE_SIZE ,
306314 max_topic_cache_size : int = MAX_TOPIC_CACHE_SIZE ,
@@ -345,6 +353,7 @@ def __init__(
345353 self .takeout = takeout
346354 self .sleep_threshold = sleep_threshold
347355 self .hide_password = hide_password
356+ self .crypto_executor_workers = crypto_executor_workers
348357 self .max_concurrent_transmissions = max_concurrent_transmissions
349358 self .max_message_cache_size = max_message_cache_size
350359 self .max_topic_cache_size = max_topic_cache_size
@@ -1072,12 +1081,13 @@ def load_plugins(self):
10721081 async def handle_download (self , packet ):
10731082 file_id , directory , file_name , in_memory , file_size , progress , progress_args = packet
10741083
1084+ max_workers = self .crypto_executor_workers
10751085 os .makedirs (directory , exist_ok = True ) if not in_memory else None
10761086 temp_file_path = os .path .abspath (re .sub ("\\ \\ " , "/" , os .path .join (directory , file_name ))) + ".temp"
10771087 file = BytesIO () if in_memory else open (temp_file_path , "wb" )
10781088
10791089 try :
1080- async for chunk in self .get_file (file_id , file_size , 0 , 0 , progress , progress_args ):
1090+ async for chunk in self .get_file (file_id , file_size , 0 , 0 , progress , progress_args , max_workers , in_memory ):
10811091 file .write (chunk )
10821092 except BaseException as e :
10831093 if not in_memory :
@@ -1108,7 +1118,9 @@ async def get_file(
11081118 limit : int = 0 ,
11091119 offset : int = 0 ,
11101120 progress : Callable = None ,
1111- progress_args : tuple = ()
1121+ progress_args : tuple = (),
1122+ max_workers : int = 1 ,
1123+ in_memory : bool = False
11121124 ) -> AsyncGenerator [bytes , None ]:
11131125 async with self .get_file_semaphore :
11141126 file_type = file_id .file_type
@@ -1151,15 +1163,39 @@ async def get_file(
11511163 )
11521164
11531165 current = 0
1166+ temporary = True
11541167 total = abs (limit ) or (1 << 31 ) - 1
11551168 chunk_size = 1024 * 1024
11561169 offset_bytes = abs (offset ) * chunk_size
11571170
11581171 dc_id = file_id .dc_id
11591172
1173+ async def handle_progress (downloaded_bytes ):
1174+ if not progress :
1175+ return
1176+ func = functools .partial (
1177+ progress ,
1178+ min (downloaded_bytes , file_size ) if file_size != 0 else downloaded_bytes ,
1179+ file_size ,
1180+ * progress_args
1181+ )
1182+ if inspect .iscoroutinefunction (progress ):
1183+ asyncio .create_task (func ())
1184+ else :
1185+ self .loop .run_in_executor (self .executor , func )
1186+
11601187 try :
1161- session = await self .get_session (dc_id , is_media = True )
1162-
1188+ try :
1189+ session = await self .get_session (dc_id , is_media = True , temporary = True )
1190+ except Exception as e :
1191+ temporary = False
1192+ try :
1193+ await session .stop ()
1194+ except :
1195+ pass
1196+ log .info (f"Reusing cached media session due to { e } " )
1197+ session = await self .get_session (dc_id , is_media = True )
1198+
11631199 r = await session .invoke (
11641200 raw .functions .upload .GetFile (
11651201 location = location ,
@@ -1170,40 +1206,119 @@ async def get_file(
11701206 )
11711207
11721208 if isinstance (r , raw .types .upload .File ):
1173- while True :
1209+ if file_size and file_size > 0 :
1210+ total_parts = math .ceil (file_size / chunk_size )
1211+ else :
1212+ if limit :
1213+ total_parts = abs (limit )
1214+ else :
1215+ total_parts = None
1216+ if total_parts is None :
11741217 chunk = r .bytes
1175-
11761218 yield chunk
1177-
11781219 current += 1
11791220 offset_bytes += chunk_size
1221+ await handle_progress (offset_bytes )
11801222
1181- if progress :
1182- func = functools .partial (
1183- progress ,
1184- min (offset_bytes , file_size )
1185- if file_size != 0
1186- else offset_bytes ,
1187- file_size ,
1188- * progress_args
1223+ while True :
1224+ if len (chunk ) < chunk_size or current >= total :
1225+ break
1226+
1227+ r = await session .invoke (
1228+ raw .functions .upload .GetFile (
1229+ location = location ,
1230+ offset = offset_bytes ,
1231+ limit = chunk_size
1232+ ),
1233+ sleep_threshold = 30
11891234 )
1190-
1191- if inspect .iscoroutinefunction (progress ):
1192- await func ()
1235+ if not isinstance (r , raw .types .upload .File ):
1236+ break
1237+ chunk = r .bytes
1238+ yield chunk
1239+ current += 1
1240+ offset_bytes += chunk_size
1241+ await handle_progress (offset_bytes )
1242+ else :
1243+ concurrency = min (max_workers , total_parts )
1244+ sem = asyncio .Semaphore (concurrency )
1245+ downloaded_bytes = 0
1246+ exceptions : list [Exception ] = []
1247+ parts_lock = asyncio .Lock ()
1248+
1249+ parts : list [dict ] = []
1250+ for i in range (total_parts ):
1251+ part = {
1252+ "part_no" : i ,
1253+ "is_completed" : asyncio .Future (),
1254+ "bytes" : None
1255+ }
1256+ parts .append (part )
1257+
1258+ if not in_memory :
1259+ tempdir = tempfile .mkdtemp (prefix = f"pyro_getfile_{ self .rnd_id ()} " )
1260+ for part in parts :
1261+ part ["bytes" ] = os .path .join (tempdir , f"part_{ part ['part_no' ]} " )
1262+
1263+ def write_file (data : bytes , path : str ):
1264+ with open (path , "wb" ) as f :
1265+ f .write (data )
1266+
1267+ def cleanup_tempdir ():
1268+ for part in parts :
1269+ try :
1270+ os .remove (part ["bytes" ])
1271+ except :
1272+ pass
1273+ try :
1274+ os .rmdir (tempdir )
1275+ except :
1276+ pass
1277+
1278+ async def download_part (part : dict ):
1279+ nonlocal downloaded_bytes
1280+ part_no = part ["part_no" ]
1281+ local_offset = offset_bytes + part_no * chunk_size
1282+ async with sem :
1283+ try :
1284+ r = await session .invoke (
1285+ raw .functions .upload .GetFile (location = location , offset = local_offset , limit = chunk_size ),
1286+ sleep_threshold = 30
1287+ )
1288+ if not isinstance (r , raw .types .upload .File ):
1289+ raise RuntimeError (f"Unexpected response for part { part_no } : { type (r )} " )
1290+
1291+ if in_memory :
1292+ part ["bytes" ] = r .bytes
1293+ else :
1294+ await self .loop .run_in_executor (self .executor , write_file , r .bytes , part ["bytes" ])
1295+
1296+ async with parts_lock :
1297+ downloaded_bytes += len (r .bytes )
1298+ await handle_progress (min (downloaded_bytes , file_size ))
1299+
1300+ part ["is_completed" ].set_result (True )
1301+ except Exception as e :
1302+ if not part ["is_completed" ].done ():
1303+ part ["is_completed" ].set_exception (e )
1304+ exceptions .append (e )
1305+
1306+ tasks = [asyncio .create_task (download_part (part )) for part in parts ]
1307+
1308+ for part in parts :
1309+ await part ["is_completed" ]
1310+ if in_memory :
1311+ yield part ["bytes" ]
11931312 else :
1194- await self .loop .run_in_executor (self .executor , func )
1313+ data = await self .loop .run_in_executor (self .executor , lambda p = part ["bytes" ]: open (p , "rb" ).read ())
1314+ yield data
11951315
1196- if len (chunk ) < chunk_size or current >= total :
1197- break
1316+ await asyncio .gather (* tasks , return_exceptions = True )
1317+ if not in_memory :
1318+ await self .loop .run_in_executor (self .executor , cleanup_tempdir )
11981319
1199- r = await session .invoke (
1200- raw .functions .upload .GetFile (
1201- location = location ,
1202- offset = offset_bytes ,
1203- limit = chunk_size
1204- ),
1205- sleep_threshold = 30
1206- )
1320+ if exceptions :
1321+ raise exceptions [0 ]
12071322
12081323 elif isinstance (r , raw .types .upload .FileCdnRedirect ):
12091324
@@ -1291,6 +1406,9 @@ def _check_all_hashes():
12911406 raise
12921407 except Exception as e :
12931408 log .exception (e )
1409+ finally :
1410+ if temporary :
1411+ await session .stop ()
12941412
12951413 async def get_session (
12961414 self ,
@@ -1367,6 +1485,10 @@ async def get_session(
13671485
13681486 if is_media :
13691487 auth_key = (await self .get_session (dc_id )).auth_key
1488+
1489+ if temporary :
1490+ await self .get_session (dc_id , is_media = True )
1491+
13701492 else :
13711493 if not is_current_dc :
13721494 auth_key = await Auth (
@@ -1386,7 +1508,8 @@ async def get_session(
13861508 port ,
13871509 auth_key ,
13881510 await self .storage .test_mode (),
1389- is_media = is_media
1511+ is_media = is_media ,
1512+ crypto_executor_workers = self .crypto_executor_workers
13901513 )
13911514
13921515 if not temporary :
0 commit comments