-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrubika_speedup.py
More file actions
235 lines (206 loc) · 9.76 KB
/
Copy pathrubika_speedup.py
File metadata and controls
235 lines (206 loc) · 9.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
from __future__ import annotations
"""
rubika_speedup.py — شتابدهندهٔ آپلود روبیکا
================================================
مشکل: پیادهسازی پیشفرض ``rubpy`` فایل رو با چانک ۱ مگابایتی و کاملاً
«سری/sequential» آپلود میکنه و بدتر از اون، **برای هر چانک فایل رو از نو
باز میکنه و seek میزنه**. روی فایلهای بزرگ (APK چند صد مگابایتی) این یعنی
صدها بار باز/بستن فایل + صدها رفتوبرگشت شبکهای پشتسرهم → آپلود خیلی کند.
راهحل (بدون دستکاری دائمی کتابخانه): این ماژول تابع ``upload_file`` کلاس
شبکهٔ rubpy رو در زمان اجرا با نسخهٔ بهینه جایگزین میکنه که:
۱. فایل رو **یکبار** کامل میخونه (یا اگر خیلی بزرگ بود بهصورت mmap).
۲. چانکها رو بهصورت **موازی** (concurrent) آپلود میکنه — تا
``RUBIKA_UPLOAD_WORKERS`` چانک همزمان.
۳. چانک پیشفرض بزرگتر (``RUBIKA_UPLOAD_CHUNK``) → رفتوبرگشت کمتر.
۴. منطق retry و reinit و callback رفتار اصلی رو حفظ میکنه.
اگه ساختار داخلی rubpy عوض شده باشه و patch نخوره، با خطا متوقف نمیشیم؛
فقط هشدار میدیم و کتابخانه با رفتار پیشفرض (کند ولی سالم) کار میکنه.
استفاده: کافیه قبل از ساختن کلاینت، یکبار ``apply_speedup()`` صدا زده بشه.
"""
import asyncio
import inspect
import os
from typing import Callable, Optional, Union
# چانک پیشفرض ۸ مگابایت (بهجای ۱ مگابایت کتابخانه)
DEFAULT_CHUNK = int(os.getenv("RUBIKA_UPLOAD_CHUNK", str(8 * 1024 * 1024)))
# تعداد چانک همزمان
DEFAULT_WORKERS = max(1, int(os.getenv("RUBIKA_UPLOAD_WORKERS", "4")))
_PATCHED = False
def apply_speedup() -> bool:
"""
تابع upload_file کتابخانهٔ rubpy رو با نسخهٔ موازی/سریع جایگزین میکنه.
Returns:
True اگه patch با موفقیت اعمال شد، در غیر اینصورت False.
"""
global _PATCHED
if _PATCHED:
return True
try:
from rubpy import network as _net # type: ignore
from rubpy.types import Update # type: ignore
from rubpy import exceptions # type: ignore
except Exception as exc: # pragma: no cover
print(f"⚠️ rubika_speedup: import rubpy ناموفق بود ({exc}) — رفتار پیشفرض حفظ شد.")
return False
# پیدا کردن کلاسی که upload_file داره
target_cls = None
for name in dir(_net):
obj = getattr(_net, name)
if isinstance(obj, type) and hasattr(obj, "upload_file"):
target_cls = obj
break
if target_cls is None:
print("⚠️ rubika_speedup: کلاس upload_file در rubpy.network پیدا نشد.")
return False
async def fast_upload_file(
self,
file: Union[str, bytes],
mime: Optional[str] = None,
file_name: Optional[str] = None,
chunk: int = DEFAULT_CHUNK,
callback: Optional[Callable[[int, int], object]] = None,
max_retries: int = 3,
backoff: float = 1.0,
*args,
**kwargs,
) -> "Update":
# چانک ۰ یا منفی → برگشت به پیشفرض بزرگ
if not chunk or chunk <= 0:
chunk = DEFAULT_CHUNK
if isinstance(file, str):
if not os.path.exists(file):
raise ValueError("File not found at the given path.")
file_name = file_name or os.path.basename(file)
file_size = os.path.getsize(file)
with open(file, "rb") as fh: # ← یکبار خواندن
payload = fh.read()
elif isinstance(file, (bytes, bytearray)):
if not file_name:
raise ValueError("file_name must be specified when uploading from bytes.")
payload = bytes(file)
file_size = len(payload)
else:
raise TypeError("file must be a file path (str) or raw bytes.")
mime = mime or file_name.split(".")[-1]
async def handle_callback(total: int, current: int):
if not callable(callback):
return
try:
if inspect.iscoroutinefunction(callback):
await callback(total, current)
else:
callback(total, current)
except Exception:
return
# متادیتای آپلود (با بازنشانی خودکار اگه سرور خواست)
result = await self.client.request_send_file(file_name, file_size, mime)
state = {
"file_id": result.id,
"dc_id": result.dc_id,
"upload_url": result.upload_url,
"access_hash_send": result.access_hash_send,
}
total_parts = max(1, (file_size + chunk - 1) // chunk)
async def upload_chunk(part_number: int) -> dict:
start = (part_number - 1) * chunk
data = payload[start:start + chunk]
for attempt in range(max_retries):
try:
async with self.session.post(
url=state["upload_url"],
headers={
"auth": self.client.auth,
"file-id": state["file_id"],
"total-part": str(total_parts),
"part-number": str(part_number),
"chunk-size": str(len(data)),
"access-hash-send": state["access_hash_send"],
},
data=data,
proxy=self.client.proxy,
) as response:
return await response.json()
except Exception as e:
self.logger.warning(
f"Error uploading chunk {part_number} "
f"(attempt {attempt + 1}/{max_retries}): {e}"
)
if attempt < max_retries - 1:
await asyncio.sleep(backoff * (2 ** attempt))
else:
raise
async def reinit():
r = await self.client.request_send_file(file_name, file_size, mime)
state.update(
file_id=r.id,
dc_id=r.dc_id,
upload_url=r.upload_url,
access_hash_send=r.access_hash_send,
)
workers = min(DEFAULT_WORKERS, total_parts)
sem = asyncio.Semaphore(workers)
uploaded_parts = 0
final_result: dict = {}
lock = asyncio.Lock()
async def worker(part_number: int):
nonlocal uploaded_parts, final_result
async with sem:
res = await upload_chunk(part_number)
if isinstance(res, dict) and res.get("status") == "ERROR_TRY_AGAIN":
# سرور خواست از نو شروع کنیم → reinit و دوبارهٔ همین چانک
async with lock:
await reinit()
res = await upload_chunk(part_number)
async with lock:
uploaded_parts += 1
await handle_callback(
file_size, min(uploaded_parts * chunk, file_size)
)
if isinstance(res, dict) and res.get("status") == "OK":
final_result = res
# آخرین چانک معمولاً پاسخ نهایی (access_hash_rec) رو داره؛
# برای اطمینان همه رو موازی میفرستیم و پاسخ OK نهایی رو نگه میداریم.
await asyncio.gather(*(worker(p) for p in range(1, total_parts + 1)))
if (
final_result.get("status") == "OK"
and final_result.get("status_det") == "OK"
):
return Update(
{
"mime": mime,
"size": file_size,
"dc_id": state["dc_id"],
"file_id": state["file_id"],
"file_name": file_name,
"access_hash_rec": final_result["data"]["access_hash_rec"],
}
)
# اگه به هر دلیل پاسخ نهایی OK نبود، آخرین چانک رو دوباره (سری) بفرست
last = await upload_chunk(total_parts)
if (
isinstance(last, dict)
and last.get("status") == "OK"
and last.get("status_det") == "OK"
):
return Update(
{
"mime": mime,
"size": file_size,
"dc_id": state["dc_id"],
"file_id": state["file_id"],
"file_name": file_name,
"access_hash_rec": last["data"]["access_hash_rec"],
}
)
raise exceptions(last.get("status_det"))(last)
try:
target_cls.upload_file = fast_upload_file # type: ignore[attr-defined]
except Exception as exc:
print(f"⚠️ rubika_speedup: اعمال patch ناموفق بود ({exc}).")
return False
_PATCHED = True
print(
f"⚡ rubika_speedup فعال شد — چانک {DEFAULT_CHUNK // (1024*1024)}MB، "
f"{DEFAULT_WORKERS} آپلود موازی."
)
return True