async-mega-py 2.2.2__tar.gz → 2.3.0__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.
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/PKG-INFO +1 -1
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/pyproject.toml +1 -1
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/__main__.py +1 -1
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/client.py +2 -2
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/core.py +13 -4
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/data_structures.py +49 -19
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/vault.py +15 -4
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/LICENSE +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/README.md +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/__init__.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/api.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/auth.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/chunker.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/crypto.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/download.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/env.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/errors.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/filesystem.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/progress/__init__.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/progress/rich_progress.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/py.typed +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/transfer_it.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/upload.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.0}/src/mega/utils.py +0 -0
|
@@ -100,8 +100,8 @@ async def dump(output_dir: Path = CWD) -> None:
|
|
|
100
100
|
fs = await mega.get_filesystem()
|
|
101
101
|
out = output_dir / "filesystem.json"
|
|
102
102
|
out.parent.mkdir(exist_ok=True)
|
|
103
|
-
logger.info(f"Creating filesystem dump at '{out!s}'")
|
|
104
103
|
out.write_text(json.dumps(fs.dump(), indent=2, ensure_ascii=False))
|
|
104
|
+
logger.info(f"Created filesystem dump at '{out!s}'")
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
@app.command()
|
|
@@ -192,7 +192,7 @@ class MegaNzClient(APIContextManager[MegaAPI]):
|
|
|
192
192
|
full_key = b64_to_a32(public_key)
|
|
193
193
|
crypto = Crypto.decompose(full_key)
|
|
194
194
|
file_info = await self._core.request_file_info(public_handle, is_public=True)
|
|
195
|
-
output_name = self._core.decrypt_attrs(file_info._at, crypto.key).name
|
|
195
|
+
output_name = self._core.decrypt_attrs(file_info._at, crypto.key, public_handle).name
|
|
196
196
|
output_path = Path(output_dir or ".") / output_name
|
|
197
197
|
return await self._core.download_file(file_info, crypto, output_path)
|
|
198
198
|
|
|
@@ -277,7 +277,7 @@ class MegaNzClient(APIContextManager[MegaAPI]):
|
|
|
277
277
|
full_key = b64_to_a32(public_key)
|
|
278
278
|
key = Crypto.decompose(full_key).key
|
|
279
279
|
file_info = await self._core.request_file_info(public_handle, is_public=True)
|
|
280
|
-
name = self._core.decrypt_attrs(file_info._at, key).name
|
|
280
|
+
name = self._core.decrypt_attrs(file_info._at, key, public_handle).name
|
|
281
281
|
return dataclasses.replace(file_info, name=name)
|
|
282
282
|
|
|
283
283
|
async def import_public_file(
|
|
@@ -6,7 +6,7 @@ import logging
|
|
|
6
6
|
import random
|
|
7
7
|
import re
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
9
|
+
from typing import TYPE_CHECKING, Any, NamedTuple, final
|
|
10
10
|
|
|
11
11
|
import yarl
|
|
12
12
|
from Crypto.Cipher import AES
|
|
@@ -23,6 +23,7 @@ from mega.crypto import (
|
|
|
23
23
|
encrypt_key,
|
|
24
24
|
)
|
|
25
25
|
from mega.data_structures import (
|
|
26
|
+
_EMPTY_ATTRS,
|
|
26
27
|
AccountStatsSerialized,
|
|
27
28
|
Attributes,
|
|
28
29
|
Crypto,
|
|
@@ -62,6 +63,7 @@ class PublicURLInfo(NamedTuple):
|
|
|
62
63
|
return self.selected_folder or self.selected_file
|
|
63
64
|
|
|
64
65
|
|
|
66
|
+
@final
|
|
65
67
|
@dataclasses.dataclass(slots=True)
|
|
66
68
|
class MegaCore:
|
|
67
69
|
api: MegaAPI
|
|
@@ -226,7 +228,7 @@ class MegaCore:
|
|
|
226
228
|
if node.type in (NodeType.FILE, NodeType.FOLDER):
|
|
227
229
|
full_key, share_key = self.vault[node]
|
|
228
230
|
crypto = Crypto.decompose(full_key, node.type, share_key)
|
|
229
|
-
attributes = self.decrypt_attrs(node._a, crypto.key)
|
|
231
|
+
attributes = self.decrypt_attrs(node._a, crypto.key, node.id)
|
|
230
232
|
|
|
231
233
|
else:
|
|
232
234
|
name = {
|
|
@@ -402,9 +404,16 @@ class MegaCore:
|
|
|
402
404
|
self.clear_cache()
|
|
403
405
|
|
|
404
406
|
@staticmethod
|
|
405
|
-
def
|
|
407
|
+
def _decrypt_attrs(attrs: str, key: tuple[int, ...]) -> Attributes:
|
|
406
408
|
return Attributes.parse(decrypt_attr(b64_url_decode(attrs), key))
|
|
407
409
|
|
|
410
|
+
@classmethod
|
|
411
|
+
def decrypt_attrs(cls, attrs: str, key: tuple[int, ...], node_id: NodeID) -> Attributes:
|
|
412
|
+
attributes = cls._decrypt_attrs(attrs, key)
|
|
413
|
+
if attributes is _EMPTY_ATTRS:
|
|
414
|
+
logger.warning("Unable to decrypt attributes of node %s", node_id)
|
|
415
|
+
return attributes
|
|
416
|
+
|
|
408
417
|
async def import_file(
|
|
409
418
|
self,
|
|
410
419
|
public_handle: NodeID,
|
|
@@ -415,7 +424,7 @@ class MegaCore:
|
|
|
415
424
|
full_key = b64_to_a32(public_key)
|
|
416
425
|
key = Crypto.decompose(full_key).key
|
|
417
426
|
file_info = await self.request_file_info(public_handle, is_public=True)
|
|
418
|
-
name = self.decrypt_attrs(file_info._at, key).name
|
|
427
|
+
name = self.decrypt_attrs(file_info._at, key, public_handle).name
|
|
419
428
|
encrypted_key = a32_to_base64(encrypt_key(full_key, self.vault.master_key))
|
|
420
429
|
attributes = b64_url_encode(encrypt_attr({"n": name}, key))
|
|
421
430
|
|
|
@@ -27,6 +27,10 @@ TimeStamp: TypeAlias = int
|
|
|
27
27
|
|
|
28
28
|
SharedKeys: TypeAlias = dict[NodeID, tuple[int, ...]]
|
|
29
29
|
|
|
30
|
+
NodeKey = tuple[int, int, int, int]
|
|
31
|
+
ComposedFileKey = tuple[int, int, int, int, int, int, int, int]
|
|
32
|
+
ComposedFolderKey = tuple[int, int, int, int]
|
|
33
|
+
|
|
30
34
|
|
|
31
35
|
class ByteSize(int):
|
|
32
36
|
def human_readable(self) -> str:
|
|
@@ -67,7 +71,7 @@ class NodeSerialized(TypedDict):
|
|
|
67
71
|
h: NodeID # ID
|
|
68
72
|
p: NodeID # Parent ID
|
|
69
73
|
u: NotRequired[UserID] # Owner (user ID), not present in transfer.it nodes
|
|
70
|
-
t: ReadOnly[Literal[0, 1, 2, 3, 4
|
|
74
|
+
t: ReadOnly[Literal[0, 1, 2, 3, 4]] # Node type
|
|
71
75
|
a: str # Serialized attributes
|
|
72
76
|
ts: TimeStamp # creation date
|
|
73
77
|
|
|
@@ -174,23 +178,23 @@ class FileInfo(_DictDumper):
|
|
|
174
178
|
|
|
175
179
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
176
180
|
class Crypto(_DictDumper):
|
|
177
|
-
key:
|
|
181
|
+
key: NodeKey
|
|
178
182
|
iv: tuple[int, int]
|
|
179
183
|
meta_mac: tuple[int, int]
|
|
180
184
|
|
|
181
|
-
full_key:
|
|
182
|
-
share_key:
|
|
185
|
+
full_key: ComposedFileKey | ComposedFolderKey
|
|
186
|
+
share_key: NodeKey | None
|
|
183
187
|
|
|
184
188
|
@classmethod
|
|
185
189
|
def compose(
|
|
186
190
|
cls,
|
|
187
|
-
key:
|
|
188
|
-
iv: tuple[int,
|
|
189
|
-
meta_mac: tuple[int,
|
|
191
|
+
key: NodeKey,
|
|
192
|
+
iv: tuple[int, int],
|
|
193
|
+
meta_mac: tuple[int, int],
|
|
190
194
|
node_type: NodeType = NodeType.FILE,
|
|
191
195
|
) -> Crypto:
|
|
192
196
|
if node_type is NodeType.FILE:
|
|
193
|
-
|
|
197
|
+
composed_key = (
|
|
194
198
|
key[0] ^ iv[0],
|
|
195
199
|
key[1] ^ iv[1],
|
|
196
200
|
key[2] ^ meta_mac[0],
|
|
@@ -198,30 +202,47 @@ class Crypto(_DictDumper):
|
|
|
198
202
|
*iv,
|
|
199
203
|
*meta_mac,
|
|
200
204
|
)
|
|
205
|
+
if len(composed_key) != 8:
|
|
206
|
+
raise RuntimeError(f"Invalid key, expected key len for files is 8, got {len(composed_key) = }")
|
|
207
|
+
|
|
201
208
|
else:
|
|
202
|
-
|
|
209
|
+
if len(key) != 4:
|
|
210
|
+
raise RuntimeError(f"Invalid key, expected key len for folders is 4, got {len(key) = }")
|
|
211
|
+
|
|
212
|
+
iv = ()
|
|
213
|
+
meta_mac = ()
|
|
214
|
+
composed_key = key
|
|
203
215
|
|
|
204
|
-
return Crypto(key, iv, meta_mac,
|
|
216
|
+
return Crypto(key, iv, meta_mac, composed_key, None) # pyright: ignore[reportArgumentType]
|
|
205
217
|
|
|
206
218
|
@classmethod
|
|
207
219
|
def decompose(
|
|
208
220
|
cls,
|
|
209
|
-
|
|
221
|
+
composed_key: ComposedFileKey | ComposedFolderKey,
|
|
210
222
|
node_type: NodeType = NodeType.FILE,
|
|
211
|
-
share_key:
|
|
223
|
+
share_key: NodeKey | None = None,
|
|
212
224
|
) -> Crypto:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
|
|
226
|
+
key = composed_key[:4]
|
|
227
|
+
iv = composed_key[4:6]
|
|
228
|
+
meta_mac = composed_key[6:8]
|
|
216
229
|
if node_type is NodeType.FILE:
|
|
230
|
+
if len(composed_key) != 8:
|
|
231
|
+
raise RuntimeError(f"Invalid key, expected key len for files is 8, got {len(composed_key) = }")
|
|
232
|
+
|
|
217
233
|
key = (
|
|
218
234
|
key[0] ^ iv[0],
|
|
219
235
|
key[1] ^ iv[1],
|
|
220
236
|
key[2] ^ meta_mac[0],
|
|
221
237
|
key[3] ^ meta_mac[1],
|
|
222
238
|
)
|
|
239
|
+
assert iv
|
|
240
|
+
assert meta_mac
|
|
241
|
+
else:
|
|
242
|
+
if len(composed_key) != 4:
|
|
243
|
+
raise RuntimeError(f"Invalid key, expected key len for folders is 4, got {len(composed_key) = }")
|
|
223
244
|
|
|
224
|
-
return Crypto(key, iv, meta_mac,
|
|
245
|
+
return Crypto(key, iv, meta_mac, composed_key, share_key) # pyright: ignore[reportArgumentType]
|
|
225
246
|
|
|
226
247
|
@classmethod
|
|
227
248
|
def from_dump(cls, dump: dict[str, Any]) -> Self:
|
|
@@ -318,6 +339,8 @@ class Attributes(_DictDumper):
|
|
|
318
339
|
|
|
319
340
|
@classmethod
|
|
320
341
|
def parse(cls, attrs: AttributesSerialized) -> Self:
|
|
342
|
+
if not attrs:
|
|
343
|
+
return _EMPTY_ATTRS
|
|
321
344
|
return cls(
|
|
322
345
|
name=attrs.get("n", ""),
|
|
323
346
|
label=_LABELS[attrs.get("lbl", 0)],
|
|
@@ -336,6 +359,9 @@ class Attributes(_DictDumper):
|
|
|
336
359
|
}
|
|
337
360
|
|
|
338
361
|
|
|
362
|
+
_EMPTY_ATTRS = Attributes("")
|
|
363
|
+
|
|
364
|
+
|
|
339
365
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
340
366
|
class AccountBalance(_DictDumper):
|
|
341
367
|
amount: float
|
|
@@ -377,6 +403,7 @@ class AccountStatsSerialized(TypedDict):
|
|
|
377
403
|
mxfer: NotRequired[int] # maximum transfer allowance
|
|
378
404
|
srvratio: float # Ratio of PRO transfer quota that is able to be served to others
|
|
379
405
|
suntil: NotRequired[TimeStamp] # Expiration time of the currently active plan
|
|
406
|
+
rtt: NotRequired[int] # ???
|
|
380
407
|
|
|
381
408
|
|
|
382
409
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
@@ -395,10 +422,13 @@ class StorageMetrics(_DictDumper):
|
|
|
395
422
|
class StorageQuota(_DictDumper):
|
|
396
423
|
used: ByteSize
|
|
397
424
|
max: ByteSize
|
|
398
|
-
|
|
399
|
-
percent: int
|
|
400
425
|
threshold: int
|
|
401
426
|
|
|
427
|
+
percent: int = dataclasses.field(init=False)
|
|
428
|
+
|
|
429
|
+
def __post_init__(self) -> None:
|
|
430
|
+
object.__setattr__(self, "percent", round(self.ratio * 100))
|
|
431
|
+
|
|
402
432
|
@property
|
|
403
433
|
def ratio(self) -> float:
|
|
404
434
|
return self.used / self.max
|
|
@@ -418,7 +448,6 @@ class StorageQuota(_DictDumper):
|
|
|
418
448
|
return cls(
|
|
419
449
|
used=used,
|
|
420
450
|
max=max,
|
|
421
|
-
percent=int(used / max * 100),
|
|
422
451
|
threshold=threshold // 100,
|
|
423
452
|
)
|
|
424
453
|
|
|
@@ -451,6 +480,7 @@ class AccountStats(_DictParser, _DictDumper):
|
|
|
451
480
|
@classmethod
|
|
452
481
|
def parse(cls, data: AccountStatsSerialized) -> Self: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
453
482
|
transfer = None
|
|
483
|
+
|
|
454
484
|
if max_transfer := data.get("mxfer"):
|
|
455
485
|
assert "caxfer" in data
|
|
456
486
|
assert "csxfer" in data
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
-
from typing import TYPE_CHECKING, Final
|
|
4
|
+
from typing import TYPE_CHECKING, Final, cast
|
|
5
5
|
|
|
6
6
|
from mega.crypto import b64_to_a32, decrypt_key
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
-
from mega.data_structures import
|
|
9
|
+
from mega.data_structures import (
|
|
10
|
+
ComposedFileKey,
|
|
11
|
+
ComposedFolderKey,
|
|
12
|
+
GetNodesResponse,
|
|
13
|
+
Node,
|
|
14
|
+
NodeID,
|
|
15
|
+
SharedKeys,
|
|
16
|
+
UserID,
|
|
17
|
+
)
|
|
10
18
|
|
|
11
19
|
|
|
12
20
|
_EXPORTED: Final = "EXP"
|
|
@@ -35,7 +43,7 @@ class MegaVault:
|
|
|
35
43
|
if key := new_keys.get(node_id):
|
|
36
44
|
self._shared_keys.setdefault(owner, {})[node_id] = key
|
|
37
45
|
|
|
38
|
-
def __getitem__(self, node: Node) -> tuple[
|
|
46
|
+
def __getitem__(self, node: Node) -> tuple[ComposedFileKey | ComposedFolderKey, tuple[int, ...] | None]:
|
|
39
47
|
share_key: tuple[int, ...] | None = None
|
|
40
48
|
|
|
41
49
|
# my files/folders
|
|
@@ -63,7 +71,10 @@ class MegaVault:
|
|
|
63
71
|
else:
|
|
64
72
|
raise RuntimeError(f"We do not have keys for {node = }")
|
|
65
73
|
|
|
66
|
-
|
|
74
|
+
if len(full_key) not in (4, 8):
|
|
75
|
+
raise RuntimeError(f"Invalid key len found for node {node.id} ({len(full_key)})")
|
|
76
|
+
|
|
77
|
+
return cast("ComposedFileKey | ComposedFolderKey", full_key), share_key
|
|
67
78
|
|
|
68
79
|
def save_public_key(self, node_id: NodeID, share_key: tuple[int, ...]) -> None:
|
|
69
80
|
self._shared_keys[_EXPORTED][node_id] = share_key
|
|
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
|