MainShortcuts2 2.7.2__tar.gz → 2.7.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/PKG-INFO +1 -1
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/pyproject.toml +1 -1
- mainshortcuts2-2.7.3/src/MainShortcuts2/_module_info.py +2 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/github.py +60 -4
- mainshortcuts2-2.7.2/src/MainShortcuts2/ex/aiohttp_ex/web.py → mainshortcuts2-2.7.3/src/MainShortcuts2/ex/aiohttp_ex/web/__init__.py +0 -55
- mainshortcuts2-2.7.3/src/MainShortcuts2/ex/aiohttp_ex/web/__main__.py +56 -0
- mainshortcuts2-2.7.3/src/MainShortcuts2/ex/urlparse_ex.py +24 -0
- mainshortcuts2-2.7.3/src/MainShortcuts2/java_ext/__init__.py +333 -0
- mainshortcuts2-2.7.3/src/MainShortcuts2/java_ext/ms2ext.py +304 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ms2hash.py +4 -2
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/utils.py +23 -22
- mainshortcuts2-2.7.2/src/MainShortcuts2/_module_info.py +0 -2
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/README.md +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/__init__.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/__main__.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/_any2json_regs.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/_ms2app_regs.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/advanced.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/any2json.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/base.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/gigachat.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/webdav.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/cfg.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/core.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/core_config.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/dict.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/dir.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/__init__.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/datetime_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/pathlib_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/pil_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/psutil_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ex/sqlite_ex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/file.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/gui_scripts/__init__.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/gui_scripts/utils.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/json.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/linux.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/list.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/ms2app.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/path.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/proc.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/regex.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/special_chars.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/sql/_sql_base.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/sql/postgresql.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/sql/sqlite.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/str.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/term.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/types.py +0 -0
- {mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/win.py +0 -0
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import requests
|
|
3
3
|
from .base import BaseClient, ObjectBase
|
|
4
4
|
from functools import cached_property
|
|
5
|
+
from MainShortcuts2 import ms
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import IO
|
|
7
8
|
from urllib.parse import urlparse
|
|
@@ -12,6 +13,7 @@ __all__ = [
|
|
|
12
13
|
"UrlInfo",
|
|
13
14
|
"User",
|
|
14
15
|
]
|
|
16
|
+
MAX_PER_PAGE = 100
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class _CatBase:
|
|
@@ -149,6 +151,15 @@ class Release(_HasUrl):
|
|
|
149
151
|
self.upload_url: str = self["upload_url"]
|
|
150
152
|
self.zipball_url: str | None = self["zipball_url"]
|
|
151
153
|
|
|
154
|
+
@property
|
|
155
|
+
def asset_names(self):
|
|
156
|
+
return {i.name for i in self.assets}
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def assets_by_name(self):
|
|
160
|
+
return {i.name: i for i in self.assets}
|
|
161
|
+
|
|
162
|
+
@property
|
|
152
163
|
def delete(self):
|
|
153
164
|
self.client.releases.delete(self.url_info.username, self.url_info.repo, self.id)
|
|
154
165
|
|
|
@@ -158,6 +169,9 @@ class Release(_HasUrl):
|
|
|
158
169
|
def update(self, **data):
|
|
159
170
|
return self.client.releases.update(self.url_info.username, self.url_info.repo, self.id, **data)
|
|
160
171
|
|
|
172
|
+
def iter_online_assets(self, count=MAX_PER_PAGE):
|
|
173
|
+
return self.client.release_assets.list_iter(self.url_info.username, self.url_info.repo, self.id, count)
|
|
174
|
+
|
|
161
175
|
|
|
162
176
|
class ReleaseAsset(_HasUrl):
|
|
163
177
|
"""Data related to a release."""
|
|
@@ -181,6 +195,22 @@ class ReleaseAsset(_HasUrl):
|
|
|
181
195
|
def delete(self):
|
|
182
196
|
self.client.release_assets.delete(self.url_info.username, self.url_info.repo, self.id)
|
|
183
197
|
|
|
198
|
+
def download(self, path, check=True, **kw):
|
|
199
|
+
kw.setdefault("session", self.client.http)
|
|
200
|
+
result = ms.utils.download_file(self.browser_download_url, path, **kw)
|
|
201
|
+
if check:
|
|
202
|
+
mpath = ms.path.Path(path)
|
|
203
|
+
if self.size != mpath.size:
|
|
204
|
+
mpath.delete()
|
|
205
|
+
raise RuntimeError("The file is corrupted (size difference)")
|
|
206
|
+
if self.digest:
|
|
207
|
+
alg, asset_hex = self.digest.split(":", 1)
|
|
208
|
+
local_hex = mpath.hash_hex(alg)
|
|
209
|
+
if asset_hex != local_hex:
|
|
210
|
+
mpath.delete()
|
|
211
|
+
raise RuntimeError("The file is corrupted (hash difference)")
|
|
212
|
+
return result
|
|
213
|
+
|
|
184
214
|
def reload(self):
|
|
185
215
|
return self.client.release_assets.get(self.url_info.username, self.url_info.repo, self.id)
|
|
186
216
|
|
|
@@ -201,11 +231,23 @@ class Client(BaseClient):
|
|
|
201
231
|
"""Get a release asset"""
|
|
202
232
|
return ReleaseAsset(self._c, self._r("GET", owner, repo, asset_id))
|
|
203
233
|
|
|
204
|
-
def list(self, owner: str, repo: str, release_id: int, per_page=
|
|
234
|
+
def list(self, owner: str, repo: str, release_id: int, per_page=MAX_PER_PAGE, page=1):
|
|
205
235
|
"""List release assets"""
|
|
206
236
|
resp: list = self._c.request("GET", f"repos/{owner}/{repo}/releases/{release_id}/assets", params={"per_page": per_page, "page": page})
|
|
207
237
|
return [ReleaseAsset(self._c, i) for i in resp]
|
|
208
238
|
|
|
239
|
+
def list_iter(self, owner: str, repo: str, release_id: int, count=100):
|
|
240
|
+
page = 1
|
|
241
|
+
completed = 0
|
|
242
|
+
while completed < count:
|
|
243
|
+
per_page = min(count - completed, MAX_PER_PAGE)
|
|
244
|
+
releases = self.list(owner, repo, release_id, per_page, page)
|
|
245
|
+
if not releases:
|
|
246
|
+
break
|
|
247
|
+
yield from releases
|
|
248
|
+
completed += len(releases)
|
|
249
|
+
page += 1
|
|
250
|
+
|
|
209
251
|
def update(self, owner: str, repo: str, asset_id: int, *,
|
|
210
252
|
label: str = None,
|
|
211
253
|
name: str = None,
|
|
@@ -297,10 +339,22 @@ class Client(BaseClient):
|
|
|
297
339
|
def get_latest(self, owner: str, repo: str):
|
|
298
340
|
return Release(self._c, self._r("GET", owner, repo, "latest"))
|
|
299
341
|
|
|
300
|
-
def list(self, owner: str, repo: str, per_page=
|
|
342
|
+
def list(self, owner: str, repo: str, per_page=MAX_PER_PAGE, page=1):
|
|
301
343
|
resp: list = self._c.request("GET", f"repos/{owner}/{repo}/releases", params={"per_page": per_page, "page": page})
|
|
302
344
|
return [Release(self._c, i) for i in resp]
|
|
303
345
|
|
|
346
|
+
def list_iter(self, owner: str, repo: str, count=100):
|
|
347
|
+
page = 1
|
|
348
|
+
completed = 0
|
|
349
|
+
while completed < count:
|
|
350
|
+
per_page = min(count - completed, MAX_PER_PAGE)
|
|
351
|
+
releases = self.list(owner, repo, per_page, page)
|
|
352
|
+
if not releases:
|
|
353
|
+
break
|
|
354
|
+
yield from releases
|
|
355
|
+
completed += len(releases)
|
|
356
|
+
page += 1
|
|
357
|
+
|
|
304
358
|
def update(self, owner: str, repo: str, release_id: int,
|
|
305
359
|
body: str = None,
|
|
306
360
|
discussion_category_name: str = None,
|
|
@@ -373,8 +427,10 @@ class Client(BaseClient):
|
|
|
373
427
|
if kw.get("token"):
|
|
374
428
|
return cls(**kw) # Токен указан в аргументах
|
|
375
429
|
if allow_no_token:
|
|
376
|
-
|
|
377
|
-
|
|
430
|
+
kw["token"] = os.environ.get("GITHUB_TOKEN")
|
|
431
|
+
else:
|
|
432
|
+
kw["token"] = os.environ["GITHUB_TOKEN"]
|
|
433
|
+
return cls(**kw)
|
|
378
434
|
|
|
379
435
|
def request(self, httpm, apim, raw=False, **kw) -> dict | list | requests.Response:
|
|
380
436
|
if raw:
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import sys
|
|
2
1
|
import typing
|
|
3
2
|
import warnings
|
|
4
3
|
from aiohttp import BasicAuth, hdrs, web
|
|
@@ -102,57 +101,3 @@ def auth_basic(header=hdrs.AUTHORIZATION, filter: typing.Callable[[BasicAuth], b
|
|
|
102
101
|
return Response(status=401, headers=basic_unauthorized_headers) # Заголовок отсутствует
|
|
103
102
|
return wrapper
|
|
104
103
|
return deco
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@ms.utils.main_func(__name__)
|
|
108
|
-
def main(args=None, **kw):
|
|
109
|
-
if args is None:
|
|
110
|
-
from argparse import ArgumentParser
|
|
111
|
-
argp = ArgumentParser()
|
|
112
|
-
argp.add_argument("--backlog", default=128, type=int)
|
|
113
|
-
argp.add_argument("--handler-cancellation", action="store_true")
|
|
114
|
-
argp.add_argument("--handler", help="функция для обработки всех запросов (module:variable)")
|
|
115
|
-
argp.add_argument("--keepalive-timeout", default=75, type=int)
|
|
116
|
-
argp.add_argument("--no-handle-signals", action="store_true")
|
|
117
|
-
argp.add_argument("--no-print", action="store_true", help="отключить вывод запросов")
|
|
118
|
-
argp.add_argument("--shutdown-timeout", default=60, type=int)
|
|
119
|
-
argp.add_argument("-H", "--host", default="127.0.0.1", help="IP для прослушивания")
|
|
120
|
-
argp.add_argument("-p", "--port", default=8080, type=int, help="порт для прослушивания")
|
|
121
|
-
argp.description = "Простой HTTP сервер, выводящий содержимое всех запросов"
|
|
122
|
-
args = argp.parse_args()
|
|
123
|
-
app = Application()
|
|
124
|
-
log = ms.utils.mini_log
|
|
125
|
-
if args.handler:
|
|
126
|
-
import importlib
|
|
127
|
-
m_name, f_name = args.handler.split(":", 1)
|
|
128
|
-
custom_handler = getattr(importlib.import_module(m_name), f_name)
|
|
129
|
-
else:
|
|
130
|
-
async def custom_handler(req: Request):
|
|
131
|
-
return 200
|
|
132
|
-
do_print = not args.no_print
|
|
133
|
-
|
|
134
|
-
@app.on_any_path_request(["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"])
|
|
135
|
-
async def _(req: Request):
|
|
136
|
-
if do_print:
|
|
137
|
-
log("Request %s %s from %s", req.method, req.raw_path, req.remote)
|
|
138
|
-
log("Headers:")
|
|
139
|
-
for k, v in req.headers.items():
|
|
140
|
-
log("- %s: %s", k, v)
|
|
141
|
-
has_content = False
|
|
142
|
-
async for chunk, _ in req.content.iter_chunks():
|
|
143
|
-
if chunk:
|
|
144
|
-
if not has_content:
|
|
145
|
-
has_content = True
|
|
146
|
-
log("Content: ")
|
|
147
|
-
sys.stderr.buffer.write(chunk)
|
|
148
|
-
log("")
|
|
149
|
-
return await custom_handler(req)
|
|
150
|
-
kw["backlog"] = args.backlog
|
|
151
|
-
kw["handle_signals"] = not args.no_handle_signals
|
|
152
|
-
kw["handler_cancellation"] = args.handler_cancellation
|
|
153
|
-
kw["host"] = args.host
|
|
154
|
-
kw["keepalive_timeout"] = args.keepalive_timeout
|
|
155
|
-
kw["port"] = args.port
|
|
156
|
-
kw["print"] = log
|
|
157
|
-
kw["shutdown_timeout"] = args.shutdown_timeout
|
|
158
|
-
app.run(**kw)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import sys
|
|
3
|
+
from MainShortcuts2.ex.aiohttp_ex.web import *
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@ms.utils.main_func(__name__)
|
|
7
|
+
def main(args=None, **kw):
|
|
8
|
+
if args is None:
|
|
9
|
+
from argparse import ArgumentParser
|
|
10
|
+
argp = ArgumentParser()
|
|
11
|
+
argp.add_argument("--backlog", default=128, type=int)
|
|
12
|
+
argp.add_argument("--handler-cancellation", action="store_true")
|
|
13
|
+
argp.add_argument("--handler", help="функция для обработки всех запросов (module:variable)")
|
|
14
|
+
argp.add_argument("--keepalive-timeout", default=75, type=int)
|
|
15
|
+
argp.add_argument("--no-handle-signals", action="store_true")
|
|
16
|
+
argp.add_argument("--no-print", action="store_true", help="отключить вывод запросов")
|
|
17
|
+
argp.add_argument("--shutdown-timeout", default=60, type=int)
|
|
18
|
+
argp.add_argument("-H", "--host", default="127.0.0.1", help="IP для прослушивания")
|
|
19
|
+
argp.add_argument("-p", "--port", default=8080, type=int, help="порт для прослушивания")
|
|
20
|
+
argp.description = "Простой HTTP сервер, выводящий содержимое всех запросов"
|
|
21
|
+
args = argp.parse_args()
|
|
22
|
+
app = Application()
|
|
23
|
+
log = ms.utils.mini_log
|
|
24
|
+
if args.handler:
|
|
25
|
+
m_name, f_name = args.handler.split(":", 1)
|
|
26
|
+
custom_handler = getattr(importlib.import_module(m_name), f_name)
|
|
27
|
+
else:
|
|
28
|
+
async def custom_handler(req: Request):
|
|
29
|
+
return 200
|
|
30
|
+
do_print = not args.no_print
|
|
31
|
+
|
|
32
|
+
@app.on_any_path_request(["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"])
|
|
33
|
+
async def _(req: Request):
|
|
34
|
+
if do_print:
|
|
35
|
+
log("Request %s %s from %s", req.method, req.raw_path, req.remote)
|
|
36
|
+
log("Headers:")
|
|
37
|
+
for k, v in req.headers.items():
|
|
38
|
+
log("- %s: %s", k, v)
|
|
39
|
+
has_content = False
|
|
40
|
+
async for chunk, _ in req.content.iter_chunks():
|
|
41
|
+
if chunk:
|
|
42
|
+
if not has_content:
|
|
43
|
+
has_content = True
|
|
44
|
+
log("Content: ")
|
|
45
|
+
sys.stderr.buffer.write(chunk)
|
|
46
|
+
log("")
|
|
47
|
+
return await custom_handler(req)
|
|
48
|
+
kw["backlog"] = args.backlog
|
|
49
|
+
kw["handle_signals"] = not args.no_handle_signals
|
|
50
|
+
kw["handler_cancellation"] = args.handler_cancellation
|
|
51
|
+
kw["host"] = args.host
|
|
52
|
+
kw["keepalive_timeout"] = args.keepalive_timeout
|
|
53
|
+
kw["port"] = args.port
|
|
54
|
+
kw["print"] = log
|
|
55
|
+
kw["shutdown_timeout"] = args.shutdown_timeout
|
|
56
|
+
app.run(**kw)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from urllib import parse
|
|
3
|
+
from types import MappingProxyType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ParseResult(parse.ParseResult):
|
|
7
|
+
@cached_property
|
|
8
|
+
def path_parts(self):
|
|
9
|
+
"""`path.split('/')`"""
|
|
10
|
+
return tuple(self.path.split("/"))
|
|
11
|
+
|
|
12
|
+
@cached_property
|
|
13
|
+
def query_dict(self):
|
|
14
|
+
return MappingProxyType(dict(self.parse_qsl(keep_blank_values=True)))
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_url(cls, url: str, **kw):
|
|
18
|
+
return cls(*parse.urlparse(url, **kw))
|
|
19
|
+
|
|
20
|
+
def parse_qs(self, **kw):
|
|
21
|
+
return parse.parse_qs(self.query, **kw)
|
|
22
|
+
|
|
23
|
+
def parse_qsl(self, **kw):
|
|
24
|
+
return parse.parse_qsl(self.query, **kw)
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
from MainShortcuts2 import ms
|
|
9
|
+
from MainShortcuts2.api import github
|
|
10
|
+
from MainShortcuts2.ex.pathlib_ex import Path
|
|
11
|
+
JAVA_VERSION_PATTERN = re.compile(r'version\s+"([^"]+)"')
|
|
12
|
+
SEARCH_RELEASE_COUNT = 1000
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AssetNotFoundError(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VersionNotFoundError(Exception):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_system_java():
|
|
24
|
+
plat = ms.advanced.get_platform()
|
|
25
|
+
suffix = ".exe" if plat.is_mustdie else ""
|
|
26
|
+
if os.environ.get("JAVA_HOME"):
|
|
27
|
+
file = (Path(os.environ["JAVA_HOME"]) / f"bin/java{suffix}").resolve()
|
|
28
|
+
else:
|
|
29
|
+
str_file = shutil.which("java")
|
|
30
|
+
if not str_file:
|
|
31
|
+
raise RuntimeError("System java not found")
|
|
32
|
+
file = Path(str_file).resolve()
|
|
33
|
+
if not file.is_file():
|
|
34
|
+
raise RuntimeError("System java not found")
|
|
35
|
+
return file
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_java_version(text: str):
|
|
39
|
+
return list(map(int, text.split("_", 1)[0].split(".")))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def detect_java_version(file: Path):
|
|
43
|
+
release_file = file.parent.parent / "release"
|
|
44
|
+
if release_file.exists():
|
|
45
|
+
try:
|
|
46
|
+
for line in release_file.read_lines_iter(True):
|
|
47
|
+
if "=" in line:
|
|
48
|
+
k, v = map(str.strip, line.split("=", 1))
|
|
49
|
+
if k == "JAVA_VERSION":
|
|
50
|
+
return _parse_java_version(v.strip('"').strip("'"))
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
try:
|
|
54
|
+
p = subprocess.run([str(file), "-version"], capture_output=True, check=True, text=True)
|
|
55
|
+
match = JAVA_VERSION_PATTERN.search(p.stderr + p.stdout)
|
|
56
|
+
return _parse_java_version(match.group(1))
|
|
57
|
+
except Exception:
|
|
58
|
+
raise RuntimeError("Couldn't determine the Java version") from None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class IncompatibleJava(Exception):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class InfoDict(dict):
|
|
66
|
+
@property
|
|
67
|
+
def builded_at(self):
|
|
68
|
+
"""Дата сборки релиза (Unix timestamp)"""
|
|
69
|
+
return self["builded_at"]
|
|
70
|
+
|
|
71
|
+
@cached_property
|
|
72
|
+
def builded_at_dt(self):
|
|
73
|
+
"""Дата сборки релиза (`datetime`)"""
|
|
74
|
+
return datetime.fromtimestamp(self.builded_at)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def classes(self) -> dict[str, str]:
|
|
78
|
+
"""Словарь классов"""
|
|
79
|
+
return self.get("classes", {})
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def max_java_version(self) -> list[int] | None:
|
|
83
|
+
"""Максимальная версия Java"""
|
|
84
|
+
return self.get("max_java_version")
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def min_java_version(self) -> list[int] | None:
|
|
88
|
+
"""Минимальная версия Java"""
|
|
89
|
+
return self.get("max_java_version")
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def name(self):
|
|
93
|
+
"""Название"""
|
|
94
|
+
return self["name"]
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def version(self):
|
|
98
|
+
"""Название версии"""
|
|
99
|
+
return self["version"]
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def version_id(self):
|
|
103
|
+
"""ID версии"""
|
|
104
|
+
return self["version_id"]
|
|
105
|
+
|
|
106
|
+
def check_java_file(self, file: Path):
|
|
107
|
+
if (self.max_java_version is None) and (self.min_java_version is None):
|
|
108
|
+
return True # Нет информации о версии
|
|
109
|
+
return self.check_java_version(detect_java_version(file))
|
|
110
|
+
|
|
111
|
+
def check_java_version(self, version: list[int]):
|
|
112
|
+
if self.max_java_version is not None:
|
|
113
|
+
if version > self.max_java_version:
|
|
114
|
+
raise IncompatibleJava(f"Incompatible Java version (current: {version}, max: {self.max_java_version})")
|
|
115
|
+
if self.min_java_version is not None:
|
|
116
|
+
if self.min_java_version > version:
|
|
117
|
+
raise IncompatibleJava(f"Incompatible Java version (current: {version}, min: {self.min_java_version})")
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
def version_id_in_range(self, min_id=0, max_id=sys.maxsize):
|
|
121
|
+
"""Проверить находится ли ID версии в диапазоне"""
|
|
122
|
+
return bool(min_id <= self.version_id < max_id)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class JavaExtManager(ms.ObjectBase):
|
|
126
|
+
"""Менеджер версий JAR файла с кешированием"""
|
|
127
|
+
|
|
128
|
+
def __init__(self, repo_owner: str, repo_name: str, github_token=None):
|
|
129
|
+
self.gh = github.Client.from_env(True, token=github_token)
|
|
130
|
+
self.repo = repo_owner, repo_name
|
|
131
|
+
self.version_cache: "dict[str,JavaExtVersion]" = {}
|
|
132
|
+
|
|
133
|
+
@cached_property
|
|
134
|
+
def dir(self):
|
|
135
|
+
"""Папка кеша"""
|
|
136
|
+
plat = ms.advanced.get_platform()
|
|
137
|
+
if plat.is_mustdie:
|
|
138
|
+
if os.environ.get("LOCALAPPDATA"):
|
|
139
|
+
base = Path(os.environ["LOCALAPPDATA"])
|
|
140
|
+
else:
|
|
141
|
+
base = Path.home() / "AppData/Local"
|
|
142
|
+
else:
|
|
143
|
+
if os.environ.get("XDG_DATA_HOME"):
|
|
144
|
+
base = Path(os.environ["XDG_DATA_HOME"])
|
|
145
|
+
else:
|
|
146
|
+
base = Path.home() / ".local/share"
|
|
147
|
+
result = base / "MainPlay_TG/MainShortcuts2/java_ext_cache_v1" / self.repo[0] / self.repo[1]
|
|
148
|
+
return result.resolve().any_mkdir()
|
|
149
|
+
|
|
150
|
+
def get_version(self, version_name: str):
|
|
151
|
+
"""Получить определённую версию (по названию)"""
|
|
152
|
+
if not version_name in self.version_cache:
|
|
153
|
+
version = JavaExtVersion(self, version_name)
|
|
154
|
+
if version.info.version == version_name:
|
|
155
|
+
self.version_cache[version_name] = version
|
|
156
|
+
return self.version_cache[version_name]
|
|
157
|
+
|
|
158
|
+
def iter_online_releases(self, count=github.MAX_PER_PAGE):
|
|
159
|
+
"""Итерация релизов с GitHub"""
|
|
160
|
+
return self.gh.releases.list_iter(*self.repo, count)
|
|
161
|
+
|
|
162
|
+
def iter_online_versions(self, count=github.MAX_PER_PAGE):
|
|
163
|
+
"""Итерация версий с GitHub"""
|
|
164
|
+
for rel in self.iter_online_releases(count):
|
|
165
|
+
if rel.tag_name.startswith("v"):
|
|
166
|
+
ver = self.get_version(rel.tag_name[1:])
|
|
167
|
+
ver.__dict__["release"] = rel # cached_property
|
|
168
|
+
yield ver
|
|
169
|
+
|
|
170
|
+
def iter_offline_versions(self):
|
|
171
|
+
"""Итерация версий из кэша"""
|
|
172
|
+
for file in self.dir.iterdir():
|
|
173
|
+
if file.suffix == ".json" and file.is_file():
|
|
174
|
+
info = InfoDict(file.read_json())
|
|
175
|
+
ver = self.get_version(info.version)
|
|
176
|
+
ver.__dict__["info"] = info # cached_property
|
|
177
|
+
yield ver
|
|
178
|
+
|
|
179
|
+
def iter_all_versions(self, online_count=github.MAX_PER_PAGE):
|
|
180
|
+
"""Итерация версий из кэша и с GitHub"""
|
|
181
|
+
yield from self.iter_offline_versions() # Сначала локальные
|
|
182
|
+
yield from self.iter_online_versions(online_count)
|
|
183
|
+
|
|
184
|
+
def search_has_class(self, class_name: str, max_count=SEARCH_RELEASE_COUNT):
|
|
185
|
+
"""Найти версию с указанным классом"""
|
|
186
|
+
return self.search_has_classes([class_name], max_count)
|
|
187
|
+
|
|
188
|
+
def search_has_classes(self, class_names: set[str], max_count=SEARCH_RELEASE_COUNT):
|
|
189
|
+
"""Найти версию, в которой есть все указанные классы"""
|
|
190
|
+
class_names = set(class_names)
|
|
191
|
+
for ver in self.iter_all_versions(max_count):
|
|
192
|
+
classes = ver.info.classes
|
|
193
|
+
if all(i in classes for i in class_names):
|
|
194
|
+
return ver
|
|
195
|
+
if len(class_names) == 1:
|
|
196
|
+
raise VersionNotFoundError("Has class: " + class_names.pop())
|
|
197
|
+
raise VersionNotFoundError("Has classes: " + ", ".join(class_names))
|
|
198
|
+
|
|
199
|
+
def search_version_id_range(self, min_id: int = 0, max_id=sys.maxsize, max_count=SEARCH_RELEASE_COUNT):
|
|
200
|
+
"""Найти версию с ID в указанном диапазоне"""
|
|
201
|
+
for ver in self.iter_all_versions(max_count):
|
|
202
|
+
if ver.info.version_id_in_range(min_id, max_id):
|
|
203
|
+
return ver
|
|
204
|
+
raise VersionNotFoundError(f"{min_id} <= version_id <= {max_id}")
|
|
205
|
+
|
|
206
|
+
def search_version_id(self, version_id: int, max_count=SEARCH_RELEASE_COUNT):
|
|
207
|
+
"""Найти версию с указанным ID"""
|
|
208
|
+
for ver in self.iter_all_versions(max_count):
|
|
209
|
+
if ver.info.version_id == version_id:
|
|
210
|
+
return ver
|
|
211
|
+
raise VersionNotFoundError(f"version_id == {version_id}")
|
|
212
|
+
|
|
213
|
+
def download_latest(self, count=1, download_jar=True):
|
|
214
|
+
"""Скачать последнюю версию (если не скачана)"""
|
|
215
|
+
for ver in self.iter_online_versions(count):
|
|
216
|
+
ver.info # Скачать файл JSON
|
|
217
|
+
if download_jar:
|
|
218
|
+
ver.jar_path # Скачать файл JAR
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class JavaExtVersion(ms.ObjectBase):
|
|
222
|
+
"""Версия JAR файла"""
|
|
223
|
+
|
|
224
|
+
def __init__(self, mgr: JavaExtManager, version_name: str):
|
|
225
|
+
self._java_bin = None
|
|
226
|
+
self._java_checked = True
|
|
227
|
+
self.mgr = mgr
|
|
228
|
+
self.version_name = version_name
|
|
229
|
+
|
|
230
|
+
@cached_property
|
|
231
|
+
def release(self):
|
|
232
|
+
"""Релиз этой версии на GitHub"""
|
|
233
|
+
return self.mgr.gh.releases.get_by_tag_name(*self.mgr.repo, "v" + self.version_name)
|
|
234
|
+
|
|
235
|
+
@cached_property
|
|
236
|
+
def info_asset(self):
|
|
237
|
+
"""Ассет файла информации на GitHub"""
|
|
238
|
+
return self._get_asset("info.json")
|
|
239
|
+
|
|
240
|
+
@cached_property
|
|
241
|
+
def info(self):
|
|
242
|
+
"""Информация о версии (кэшируется)"""
|
|
243
|
+
if self.info_path.exists():
|
|
244
|
+
return InfoDict(self.info_path.read_json())
|
|
245
|
+
with ms.utils.request("GET", self.info_asset.browser_download_url, session=self.mgr.gh.http) as resp:
|
|
246
|
+
data = InfoDict(resp.json())
|
|
247
|
+
self.info_path.write_json(data)
|
|
248
|
+
return data
|
|
249
|
+
|
|
250
|
+
@cached_property
|
|
251
|
+
def info_path(self):
|
|
252
|
+
"""Путь к файлу информации"""
|
|
253
|
+
return self.mgr.dir / f"{self.version_name}.json"
|
|
254
|
+
|
|
255
|
+
@cached_property
|
|
256
|
+
def jar_asset(self):
|
|
257
|
+
"""Ассет JAR файла на GitHub"""
|
|
258
|
+
return self._get_asset(f"{self.info.name}-{self.info.version}.jar")
|
|
259
|
+
|
|
260
|
+
@cached_property
|
|
261
|
+
def _jar_path(self):
|
|
262
|
+
"""Файл JAR (без скачивания)"""
|
|
263
|
+
return self.mgr.dir / f"{self.version_name}.jar"
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def jar_path(self):
|
|
267
|
+
"""Файл JAR (кэшируется)"""
|
|
268
|
+
if not self._jar_path.exists():
|
|
269
|
+
ms.utils.mini_log("[MainShortcuts2/java_ext] Downloading %s from %s/%s (%s)", self._jar_path.name, *self.mgr.repo, self.jar_asset.browser_download_url)
|
|
270
|
+
self.jar_asset.download(self._jar_path)
|
|
271
|
+
return self._jar_path
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def java_bin(self) -> Path:
|
|
275
|
+
"""Путь к исполняемому файлу Java (можно изменить)"""
|
|
276
|
+
if self._java_bin is None:
|
|
277
|
+
self._java_bin = find_system_java()
|
|
278
|
+
self._java_checked = False
|
|
279
|
+
return self._java_bin
|
|
280
|
+
|
|
281
|
+
@java_bin.setter
|
|
282
|
+
def java_bin(self, v):
|
|
283
|
+
if v is None:
|
|
284
|
+
self._java_bin = None
|
|
285
|
+
return
|
|
286
|
+
file = Path(v).resolve()
|
|
287
|
+
if not file.exists():
|
|
288
|
+
raise FileNotFoundError(file)
|
|
289
|
+
self._java_bin = file
|
|
290
|
+
self._java_checked = False
|
|
291
|
+
|
|
292
|
+
def _get_asset(self, name: str):
|
|
293
|
+
for i in self.release.assets:
|
|
294
|
+
if i.name == name:
|
|
295
|
+
return i
|
|
296
|
+
raise AssetNotFoundError(name)
|
|
297
|
+
|
|
298
|
+
def delete_cache(self, keep_info=True):
|
|
299
|
+
"""Удалить версию из кэша (будет скачана автоматически при использовании"""
|
|
300
|
+
self._jar_path.remove()
|
|
301
|
+
if not keep_info:
|
|
302
|
+
self.info_path.remove()
|
|
303
|
+
|
|
304
|
+
def _popen(self, args, **kw):
|
|
305
|
+
if not self._java_checked:
|
|
306
|
+
self.info.check_java_file(self.java_bin)
|
|
307
|
+
self._java_checked = True
|
|
308
|
+
return subprocess.Popen([str(self.java_bin), *args], **kw)
|
|
309
|
+
|
|
310
|
+
def _run(self, args, **kw) -> subprocess.CompletedProcess:
|
|
311
|
+
kw.setdefault("check", True)
|
|
312
|
+
if not self._java_checked:
|
|
313
|
+
self.info.check_java_file(self.java_bin)
|
|
314
|
+
self._java_checked = True
|
|
315
|
+
return subprocess.run([str(self.java_bin), *args], **kw)
|
|
316
|
+
|
|
317
|
+
def popen_class_raw(self, class_path: str, args: list[str] = [], **kw):
|
|
318
|
+
return self._popen(["-cp", str(self.jar_path), class_path, *args], **kw)
|
|
319
|
+
|
|
320
|
+
def popen_class(self, class_name: str, args: list[str] = [], **kw):
|
|
321
|
+
return self.popen_class_raw(self.info.classes[class_name], args, **kw)
|
|
322
|
+
|
|
323
|
+
def popen(self, args: list[str], **kw):
|
|
324
|
+
return self._popen(["-jar", str(self.jar_path), *args], **kw)
|
|
325
|
+
|
|
326
|
+
def run_class_raw(self, class_path: str, args: list[str] = [], **kw):
|
|
327
|
+
return self._run(["-cp", str(self.jar_path), class_path, *args], **kw)
|
|
328
|
+
|
|
329
|
+
def run_class(self, class_name: str, args: list[str] = [], **kw):
|
|
330
|
+
return self.run_class_raw(self.info.classes[class_name], args, **kw)
|
|
331
|
+
|
|
332
|
+
def run(self, args: list[str], **kw):
|
|
333
|
+
return self._run(["-jar", str(self.jar_path), *args], **kw)
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Функции для работы с большими файлами с помощью расширения на Java. При недоступности расширения будут использоваться встроенные функции"""
|
|
2
|
+
import base64
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import requests
|
|
6
|
+
import subprocess
|
|
7
|
+
import typing
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from contextlib import ExitStack
|
|
10
|
+
from MainShortcuts2 import java_ext
|
|
11
|
+
from MainShortcuts2 import ms
|
|
12
|
+
from MainShortcuts2.ex.pathlib_ex import Path
|
|
13
|
+
mgr = java_ext.JavaExtManager("MainPlay-TG", "MS2Ext.java")
|
|
14
|
+
class2version: dict[str, java_ext.JavaExtVersion] = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_ver(class_name):
|
|
18
|
+
if not class_name in class2version:
|
|
19
|
+
class2version[class_name] = mgr.search_has_class(class_name)
|
|
20
|
+
return class2version[class_name]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def popen_class(class_name, **kw) -> subprocess.Popen[bytes]:
|
|
24
|
+
kw["stderr"] = subprocess.PIPE
|
|
25
|
+
kw["stdin"] = subprocess.PIPE
|
|
26
|
+
return get_ver(class_name).popen_class(class_name, **kw)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExtCoreError(subprocess.CalledProcessError):
|
|
30
|
+
"""Ошибка при запуске расширения (повтор через fallback)"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ExtOperationError(subprocess.CalledProcessError):
|
|
35
|
+
"""Ошибка при работе расширения (конечная ошибка)"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
ErrorsForFallback = (
|
|
40
|
+
ExtCoreError, # Не удалось запустить расширение
|
|
41
|
+
java_ext.IncompatibleJava, # Несовместимая Java
|
|
42
|
+
requests.exceptions.RequestException, # Не удалось скачать расширение
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_class(class_name, config, timeout=None, **kw):
|
|
47
|
+
with popen_class(class_name) as p:
|
|
48
|
+
p.stdin.write(ms.json.encode(config).encode("utf-8"))
|
|
49
|
+
p.stdin.flush()
|
|
50
|
+
p.stdin.close()
|
|
51
|
+
if p.wait(timeout):
|
|
52
|
+
try:
|
|
53
|
+
stderr = p.stderr.read().decode("utf-8")
|
|
54
|
+
err_data = ms.json.decode(stderr)
|
|
55
|
+
except Exception:
|
|
56
|
+
raise ExtCoreError(p.returncode, p.args, p.stdout, p.stderr)
|
|
57
|
+
raise ExtOperationError(p.returncode, p.args, p.stdout, err_data)
|
|
58
|
+
stderr = p.stderr.read()
|
|
59
|
+
if not stderr:
|
|
60
|
+
return {}
|
|
61
|
+
return ms.json.decode(stderr.decode("utf-8"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class configs:
|
|
65
|
+
class _Base(dict):
|
|
66
|
+
def __setitem__(self, k, v):
|
|
67
|
+
if v is None:
|
|
68
|
+
return
|
|
69
|
+
dict.__setitem__(self, k, v)
|
|
70
|
+
|
|
71
|
+
def get(self, k, d=None):
|
|
72
|
+
if k in self:
|
|
73
|
+
return self[k] or d
|
|
74
|
+
return d
|
|
75
|
+
|
|
76
|
+
class FileDownloaderV1(_Base):
|
|
77
|
+
@property
|
|
78
|
+
def bufSize(self) -> int:
|
|
79
|
+
return self.get("bufSize") or 1024 * 32
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def checkStatus(self) -> bool:
|
|
83
|
+
return self.get("checkStatus", True)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def followRedirects(self) -> bool:
|
|
87
|
+
return self.get("followRedirects", True)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def headers(self) -> dict[str, str]:
|
|
91
|
+
return self.get("headers") or {}
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def method(self) -> str:
|
|
95
|
+
return self.get("method") or "GET"
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def path(self) -> str:
|
|
99
|
+
return self["path"]
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def query(self) -> dict[str, str]:
|
|
103
|
+
return self.get("query") or {}
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def sizeLimit(self) -> int | None:
|
|
107
|
+
return self.get("sizeLimit")
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def url(self) -> str:
|
|
111
|
+
return self["url"]
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def verify(self) -> bool:
|
|
115
|
+
return self.get("verify", True)
|
|
116
|
+
|
|
117
|
+
class _FileHasherBase(_Base):
|
|
118
|
+
@property
|
|
119
|
+
def algs(self) -> list[str]:
|
|
120
|
+
return self["algs"]
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def bufSize(self) -> int:
|
|
124
|
+
return self.get("bufSize") or 1024 * 1024 * 4
|
|
125
|
+
|
|
126
|
+
class FileHasherV1(_FileHasherBase):
|
|
127
|
+
@property
|
|
128
|
+
def path(self) -> str:
|
|
129
|
+
return self["path"]
|
|
130
|
+
|
|
131
|
+
class FileHasherV2(_FileHasherBase):
|
|
132
|
+
@property
|
|
133
|
+
def maxThreads(self) -> int:
|
|
134
|
+
return self.get("maxThreads") or 8
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def paths(self) -> list[str]:
|
|
138
|
+
return self["paths"]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class results:
|
|
142
|
+
class FileDownloaderV1(dict):
|
|
143
|
+
@property
|
|
144
|
+
def downloadedSize(self) -> int:
|
|
145
|
+
return self["downloadedSize"]
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def headers(self) -> dict[str, list[str]]:
|
|
149
|
+
return self["headers"]
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def statusCode(self) -> int:
|
|
153
|
+
return self["statusCode"]
|
|
154
|
+
|
|
155
|
+
class FileHasherV1(dict[str, bytes]):
|
|
156
|
+
@classmethod
|
|
157
|
+
def _from_b64(cls, data: dict[str, str]):
|
|
158
|
+
return cls({k: base64.b64decode(v) for k, v in data.items()})
|
|
159
|
+
FileHasherV2 = dict[str, FileHasherV1]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class real:
|
|
163
|
+
@classmethod
|
|
164
|
+
def file_downloader_v1(cls, config: configs.FileDownloaderV1, **kw):
|
|
165
|
+
return results.FileDownloaderV1(run_class("file_downloader_v1", config, **kw))
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def file_hasher_v1(cls, config: configs.FileHasherV1, **kw):
|
|
169
|
+
if not config.get("algs"):
|
|
170
|
+
return results.FileHasherV1()
|
|
171
|
+
return results.FileHasherV1._from_b64(run_class("file_hasher_v1", config, **kw))
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def file_hasher_v2(cls, config: configs.FileHasherV2, **kw):
|
|
175
|
+
if not config.get("paths"):
|
|
176
|
+
return results.FileHasherV2()
|
|
177
|
+
if not config.get("algs"):
|
|
178
|
+
return results.FileHasherV2({i: results.FileHasherV1() for i in config.paths})
|
|
179
|
+
result = run_class("file_hasher_v2", config, **kw)
|
|
180
|
+
return results.FileHasherV2({k: results.FileHasherV1._from_b64(v) for k, v in result.items()})
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class fallback:
|
|
184
|
+
@classmethod
|
|
185
|
+
def file_downloader_v1(cls, config: configs.FileDownloaderV1):
|
|
186
|
+
file = Path(config.path).resolve()
|
|
187
|
+
with ExitStack() as stack:
|
|
188
|
+
resp = stack.enter_context(requests.request(config.method, config.url,
|
|
189
|
+
allow_redirects=config.followRedirects,
|
|
190
|
+
headers=config.headers,
|
|
191
|
+
params=config.query,
|
|
192
|
+
stream=True,
|
|
193
|
+
verify=config.verify,
|
|
194
|
+
))
|
|
195
|
+
if config.checkStatus:
|
|
196
|
+
resp.raise_for_status()
|
|
197
|
+
tmp = stack.enter_context(ms.path.TempFiles(file)) # Авто удаление недокачанного файла
|
|
198
|
+
f = stack.enter_context(file.open("wb")) # Открытие файла
|
|
199
|
+
cls._download_file(resp.iter_content(config.bufSize), f.write, config.sizeLimit) # Скачивание файла
|
|
200
|
+
tmp.files.clear() # Отключить авто удаление
|
|
201
|
+
result = results.FileDownloaderV1()
|
|
202
|
+
result["statusCode"] = resp.status_code
|
|
203
|
+
result["downloadedSize"] = f.tell()
|
|
204
|
+
result["headers"] = {k: [v] for k, v in resp.headers.items()}
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def _download_file(cls, stream: typing.Iterable[bytes], write_func: typing.Callable[[bytes], None], sizeLimit: int | None):
|
|
209
|
+
if sizeLimit is None:
|
|
210
|
+
for buf in stream:
|
|
211
|
+
write_func(buf)
|
|
212
|
+
return
|
|
213
|
+
downloaded = 0
|
|
214
|
+
for buf in stream:
|
|
215
|
+
downloaded += len(buf)
|
|
216
|
+
if sizeLimit > downloaded:
|
|
217
|
+
raise OverflowError("The file is too big")
|
|
218
|
+
write_func(buf)
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def file_hasher_v1(cls, config: configs.FileHasherV1):
|
|
222
|
+
if not config.get("algs"):
|
|
223
|
+
return results.FileHasherV1()
|
|
224
|
+
return cls._hash_file(Path(config.path), set(config.algs), config.bufSize)
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def file_hasher_v2(cls, config: configs.FileHasherV2):
|
|
228
|
+
if not config.get("paths"):
|
|
229
|
+
return results.FileHasherV2()
|
|
230
|
+
if not config.get("algs"):
|
|
231
|
+
return results.FileHasherV2({i: results.FileHasherV1() for i in config.paths})
|
|
232
|
+
algs, bufsize = set(config.algs), config.bufSize
|
|
233
|
+
with ThreadPoolExecutor(config.maxThreads) as pool:
|
|
234
|
+
futures = {i: pool.submit(cls._hash_file, Path(i), algs, bufsize) for i in config.paths}
|
|
235
|
+
return results.FileHasherV2({k: v.result() for k, v in futures.items()})
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def _hash_file(cls, file: Path, algs: set[str], bufSize: int):
|
|
239
|
+
hashmap = {i: hashlib.new(i) for i in algs}
|
|
240
|
+
update_func = [i.update for i in hashmap.values()]
|
|
241
|
+
with file.open("rb") as f:
|
|
242
|
+
read_func = f.read
|
|
243
|
+
while True:
|
|
244
|
+
buf = read_func(bufSize)
|
|
245
|
+
if not buf:
|
|
246
|
+
break
|
|
247
|
+
for uf in update_func:
|
|
248
|
+
uf(buf)
|
|
249
|
+
return results.FileHasherV1({k: v.digest() for k, v in hashmap.items()})
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def download_file(url: str, path: os.PathLike, *,
|
|
253
|
+
bufSize: int = None,
|
|
254
|
+
checkStatus=True,
|
|
255
|
+
followRedirects=True,
|
|
256
|
+
headers: dict[str, str] = None,
|
|
257
|
+
method="GET",
|
|
258
|
+
query: dict[str, str] = None,
|
|
259
|
+
sizeLimit: int = None,
|
|
260
|
+
verify=True,
|
|
261
|
+
**kw):
|
|
262
|
+
"""Скачать большой файл по HTTP"""
|
|
263
|
+
config = configs.FileDownloaderV1()
|
|
264
|
+
config["bufSize"] = bufSize
|
|
265
|
+
config["checkStatus"] = checkStatus
|
|
266
|
+
config["followRedirects"] = followRedirects
|
|
267
|
+
config["headers"] = headers
|
|
268
|
+
config["method"] = method
|
|
269
|
+
config["path"] = os.fspath(path)
|
|
270
|
+
config["query"] = query
|
|
271
|
+
config["sizeLimit"] = sizeLimit
|
|
272
|
+
config["url"] = url
|
|
273
|
+
config["verify"] = verify
|
|
274
|
+
if config.sizeLimit:
|
|
275
|
+
if config.bufSize > config.sizeLimit:
|
|
276
|
+
return fallback.file_downloader_v1(config)
|
|
277
|
+
try:
|
|
278
|
+
return real.file_downloader_v1(config, **kw)
|
|
279
|
+
except ErrorsForFallback:
|
|
280
|
+
return fallback.file_downloader_v1(config)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def hash_file(path: os.PathLike, algs: set[str], *, bufSize: int = None, **kw):
|
|
284
|
+
"""Хешировать большой файл"""
|
|
285
|
+
config = configs.FileHasherV1()
|
|
286
|
+
config["algs"] = list(algs)
|
|
287
|
+
config["bufSize"] = bufSize
|
|
288
|
+
config["path"] = os.fspath(path)
|
|
289
|
+
try:
|
|
290
|
+
return real.file_hasher_v1(config, **kw)
|
|
291
|
+
except ErrorsForFallback:
|
|
292
|
+
return fallback.file_hasher_v1(config)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def hash_many_files(paths: set[os.PathLike], algs: set[str], *, bufSize: int = None, **kw):
|
|
296
|
+
"""Хешировать несколько файлов"""
|
|
297
|
+
config = configs.FileHasherV2()
|
|
298
|
+
config["algs"] = list(algs)
|
|
299
|
+
config["bufSize"] = bufSize
|
|
300
|
+
config["paths"] = [os.fspath(i) for i in paths]
|
|
301
|
+
try:
|
|
302
|
+
return real.file_hasher_v2(config, **kw)
|
|
303
|
+
except ErrorsForFallback:
|
|
304
|
+
return fallback.file_hasher_v2(config)
|
|
@@ -86,7 +86,8 @@ class Format1:
|
|
|
86
86
|
|
|
87
87
|
def hash_gen(args: argparse.Namespace = None):
|
|
88
88
|
if args is None:
|
|
89
|
-
argp = argparse.ArgumentParser("ms2-hash_gen", description="
|
|
89
|
+
argp = argparse.ArgumentParser("ms2-hash_gen", description="Создание контрольной суммы для файла")
|
|
90
|
+
argp.epilog = "Написано на Python"
|
|
90
91
|
argp.add_argument("files", nargs="+", help="пути к файлам")
|
|
91
92
|
argp.add_argument("-b", "--bar", action="store_true", help="показывать прогрессбар (нужен модуль progressbar2)")
|
|
92
93
|
argp.add_argument("-f", "--force", action="store_true", help="перезаписывать существующие хеши")
|
|
@@ -113,7 +114,8 @@ def hash_gen(args: argparse.Namespace = None):
|
|
|
113
114
|
|
|
114
115
|
def hash_check(args: argparse.Namespace = None):
|
|
115
116
|
if args is None:
|
|
116
|
-
argp = argparse.ArgumentParser("ms2-hash_check", description="
|
|
117
|
+
argp = argparse.ArgumentParser("ms2-hash_check", description="Проверка размера и контрольной суммы файла")
|
|
118
|
+
argp.epilog = "Написано на Python"
|
|
117
119
|
argp.add_argument("files", nargs="+", help="пути к файлам")
|
|
118
120
|
argp.add_argument("-b", "--bar", action="store_true", help="показывать прогрессбар (нужен модуль progressbar2)")
|
|
119
121
|
args = argp.parse_args()
|
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
import sys
|
|
8
8
|
import time
|
|
9
9
|
from .core import ms
|
|
10
|
+
from contextlib import ExitStack
|
|
10
11
|
from functools import wraps
|
|
11
12
|
from typing import *
|
|
12
13
|
from warnings import warn
|
|
@@ -167,12 +168,12 @@ async def async_download_file(url: str, path: str, *, cb_end=return_None, cb_pro
|
|
|
167
168
|
"""Асинхронная функция для скачивания файла | `aiohttp`"""
|
|
168
169
|
kw.setdefault("method", "GET")
|
|
169
170
|
kw["url"] = url
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
with ExitStack() as stack:
|
|
172
|
+
async with async_request(**kw) as resp: # type: ignore
|
|
173
|
+
if callable(getattr(path, "write", None)):
|
|
174
|
+
f: IO[bytes] = path
|
|
175
|
+
else:
|
|
176
|
+
f = stack.enter_context(open(path, "wb"))
|
|
176
177
|
size = 0
|
|
177
178
|
await cb_start(f, resp, size)
|
|
178
179
|
try:
|
|
@@ -311,25 +312,25 @@ def sync_download_file(url: str, path: str, *, cb_end=return_None, cb_progress=r
|
|
|
311
312
|
kw.setdefault("method", "GET")
|
|
312
313
|
kw["stream"] = True
|
|
313
314
|
kw["url"] = url
|
|
314
|
-
with
|
|
315
|
+
with ExitStack() as stack:
|
|
316
|
+
resp = stack.enter_context(sync_request(**kw))
|
|
315
317
|
if callable(getattr(path, "write", None)):
|
|
316
318
|
f: IO[bytes] = path
|
|
317
319
|
else:
|
|
318
|
-
f = open(path, "wb")
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
cb_end(f, resp, size)
|
|
320
|
+
f = stack.enter_context(open(path, "wb"))
|
|
321
|
+
size = 0
|
|
322
|
+
cb_start(f, resp, size)
|
|
323
|
+
try:
|
|
324
|
+
for chunk in resp.iter_content(chunk_size):
|
|
325
|
+
size += f.write(chunk)
|
|
326
|
+
cb_progress(f, resp, size)
|
|
327
|
+
except:
|
|
328
|
+
f.close()
|
|
329
|
+
if delete_on_error:
|
|
330
|
+
if os.path.isfile(path):
|
|
331
|
+
os.remove(path)
|
|
332
|
+
raise
|
|
333
|
+
cb_end(f, resp, size)
|
|
333
334
|
return size
|
|
334
335
|
|
|
335
336
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/russian_trusted_root_ca_pem.crt
RENAMED
|
File without changes
|
{mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/api/russian_trusted_sub_ca_pem.crt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainshortcuts2-2.7.2 → mainshortcuts2-2.7.3}/src/MainShortcuts2/gui_scripts/ms2_hash_gen.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|