async-mega-py 2.2.2__tar.gz → 2.3.1__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.1}/PKG-INFO +1 -1
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/pyproject.toml +1 -1
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/__main__.py +1 -1
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/client.py +2 -2
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/core.py +13 -4
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/data_structures.py +47 -19
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/vault.py +20 -4
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/LICENSE +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/README.md +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/__init__.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/api.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/auth.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/chunker.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/crypto.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/download.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/env.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/errors.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/filesystem.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/progress/__init__.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/progress/rich_progress.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/py.typed +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/transfer_it.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/upload.py +0 -0
- {async_mega_py-2.2.2 → async_mega_py-2.3.1}/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,9 @@ 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
|
+
|
|
30
33
|
|
|
31
34
|
class ByteSize(int):
|
|
32
35
|
def human_readable(self) -> str:
|
|
@@ -67,7 +70,7 @@ class NodeSerialized(TypedDict):
|
|
|
67
70
|
h: NodeID # ID
|
|
68
71
|
p: NodeID # Parent ID
|
|
69
72
|
u: NotRequired[UserID] # Owner (user ID), not present in transfer.it nodes
|
|
70
|
-
t: ReadOnly[Literal[0, 1, 2, 3, 4
|
|
73
|
+
t: ReadOnly[Literal[0, 1, 2, 3, 4]] # Node type
|
|
71
74
|
a: str # Serialized attributes
|
|
72
75
|
ts: TimeStamp # creation date
|
|
73
76
|
|
|
@@ -174,23 +177,23 @@ class FileInfo(_DictDumper):
|
|
|
174
177
|
|
|
175
178
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
176
179
|
class Crypto(_DictDumper):
|
|
177
|
-
key:
|
|
180
|
+
key: NodeKey
|
|
178
181
|
iv: tuple[int, int]
|
|
179
182
|
meta_mac: tuple[int, int]
|
|
180
183
|
|
|
181
|
-
full_key:
|
|
182
|
-
share_key:
|
|
184
|
+
full_key: ComposedFileKey | NodeKey
|
|
185
|
+
share_key: NodeKey | None
|
|
183
186
|
|
|
184
187
|
@classmethod
|
|
185
188
|
def compose(
|
|
186
189
|
cls,
|
|
187
|
-
key:
|
|
188
|
-
iv: tuple[int,
|
|
189
|
-
meta_mac: tuple[int,
|
|
190
|
+
key: NodeKey,
|
|
191
|
+
iv: tuple[int, int],
|
|
192
|
+
meta_mac: tuple[int, int],
|
|
190
193
|
node_type: NodeType = NodeType.FILE,
|
|
191
194
|
) -> Crypto:
|
|
192
195
|
if node_type is NodeType.FILE:
|
|
193
|
-
|
|
196
|
+
composed_key = (
|
|
194
197
|
key[0] ^ iv[0],
|
|
195
198
|
key[1] ^ iv[1],
|
|
196
199
|
key[2] ^ meta_mac[0],
|
|
@@ -198,30 +201,46 @@ class Crypto(_DictDumper):
|
|
|
198
201
|
*iv,
|
|
199
202
|
*meta_mac,
|
|
200
203
|
)
|
|
204
|
+
if len(composed_key) != 8:
|
|
205
|
+
raise RuntimeError(f"Invalid key, expected key len for files is 8, got {len(composed_key) = }")
|
|
206
|
+
|
|
201
207
|
else:
|
|
202
|
-
|
|
208
|
+
iv = ()
|
|
209
|
+
meta_mac = ()
|
|
210
|
+
composed_key = key
|
|
211
|
+
if len(composed_key) != 4:
|
|
212
|
+
raise RuntimeError(f"Invalid key, expected key len for folders is 4, got {len(composed_key) = }")
|
|
203
213
|
|
|
204
|
-
return Crypto(key, iv, meta_mac,
|
|
214
|
+
return Crypto(key, iv, meta_mac, composed_key, None) # pyright: ignore[reportArgumentType]
|
|
205
215
|
|
|
206
216
|
@classmethod
|
|
207
217
|
def decompose(
|
|
208
218
|
cls,
|
|
209
|
-
|
|
219
|
+
composed_key: ComposedFileKey | NodeKey,
|
|
210
220
|
node_type: NodeType = NodeType.FILE,
|
|
211
|
-
share_key:
|
|
221
|
+
share_key: NodeKey | None = None,
|
|
212
222
|
) -> Crypto:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
223
|
+
|
|
224
|
+
key = composed_key[:4]
|
|
225
|
+
iv = composed_key[4:6]
|
|
226
|
+
meta_mac = composed_key[6:8]
|
|
216
227
|
if node_type is NodeType.FILE:
|
|
228
|
+
if len(composed_key) != 8:
|
|
229
|
+
raise RuntimeError(f"Invalid key, expected key len for files is 8, got {len(composed_key) = }")
|
|
230
|
+
|
|
217
231
|
key = (
|
|
218
232
|
key[0] ^ iv[0],
|
|
219
233
|
key[1] ^ iv[1],
|
|
220
234
|
key[2] ^ meta_mac[0],
|
|
221
235
|
key[3] ^ meta_mac[1],
|
|
222
236
|
)
|
|
237
|
+
assert iv
|
|
238
|
+
assert meta_mac
|
|
239
|
+
else:
|
|
240
|
+
if len(composed_key) != 4:
|
|
241
|
+
raise RuntimeError(f"Invalid key, expected key len for folders is 4, got {len(composed_key) = }")
|
|
223
242
|
|
|
224
|
-
return Crypto(key, iv, meta_mac,
|
|
243
|
+
return Crypto(key, iv, meta_mac, composed_key, share_key) # pyright: ignore[reportArgumentType]
|
|
225
244
|
|
|
226
245
|
@classmethod
|
|
227
246
|
def from_dump(cls, dump: dict[str, Any]) -> Self:
|
|
@@ -318,6 +337,8 @@ class Attributes(_DictDumper):
|
|
|
318
337
|
|
|
319
338
|
@classmethod
|
|
320
339
|
def parse(cls, attrs: AttributesSerialized) -> Self:
|
|
340
|
+
if not attrs:
|
|
341
|
+
return _EMPTY_ATTRS
|
|
321
342
|
return cls(
|
|
322
343
|
name=attrs.get("n", ""),
|
|
323
344
|
label=_LABELS[attrs.get("lbl", 0)],
|
|
@@ -336,6 +357,9 @@ class Attributes(_DictDumper):
|
|
|
336
357
|
}
|
|
337
358
|
|
|
338
359
|
|
|
360
|
+
_EMPTY_ATTRS = Attributes("")
|
|
361
|
+
|
|
362
|
+
|
|
339
363
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
340
364
|
class AccountBalance(_DictDumper):
|
|
341
365
|
amount: float
|
|
@@ -377,6 +401,7 @@ class AccountStatsSerialized(TypedDict):
|
|
|
377
401
|
mxfer: NotRequired[int] # maximum transfer allowance
|
|
378
402
|
srvratio: float # Ratio of PRO transfer quota that is able to be served to others
|
|
379
403
|
suntil: NotRequired[TimeStamp] # Expiration time of the currently active plan
|
|
404
|
+
rtt: NotRequired[int] # ???
|
|
380
405
|
|
|
381
406
|
|
|
382
407
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
@@ -395,10 +420,13 @@ class StorageMetrics(_DictDumper):
|
|
|
395
420
|
class StorageQuota(_DictDumper):
|
|
396
421
|
used: ByteSize
|
|
397
422
|
max: ByteSize
|
|
398
|
-
|
|
399
|
-
percent: int
|
|
400
423
|
threshold: int
|
|
401
424
|
|
|
425
|
+
percent: int = dataclasses.field(init=False)
|
|
426
|
+
|
|
427
|
+
def __post_init__(self) -> None:
|
|
428
|
+
object.__setattr__(self, "percent", round(self.ratio * 100))
|
|
429
|
+
|
|
402
430
|
@property
|
|
403
431
|
def ratio(self) -> float:
|
|
404
432
|
return self.used / self.max
|
|
@@ -418,7 +446,6 @@ class StorageQuota(_DictDumper):
|
|
|
418
446
|
return cls(
|
|
419
447
|
used=used,
|
|
420
448
|
max=max,
|
|
421
|
-
percent=int(used / max * 100),
|
|
422
449
|
threshold=threshold // 100,
|
|
423
450
|
)
|
|
424
451
|
|
|
@@ -451,6 +478,7 @@ class AccountStats(_DictParser, _DictDumper):
|
|
|
451
478
|
@classmethod
|
|
452
479
|
def parse(cls, data: AccountStatsSerialized) -> Self: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
453
480
|
transfer = None
|
|
481
|
+
|
|
454
482
|
if max_transfer := data.get("mxfer"):
|
|
455
483
|
assert "caxfer" in data
|
|
456
484
|
assert "csxfer" in data
|
|
@@ -1,18 +1,31 @@
|
|
|
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, NamedTuple, 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
|
+
GetNodesResponse,
|
|
12
|
+
Node,
|
|
13
|
+
NodeID,
|
|
14
|
+
NodeKey,
|
|
15
|
+
SharedKeys,
|
|
16
|
+
UserID,
|
|
17
|
+
)
|
|
10
18
|
|
|
11
19
|
|
|
12
20
|
_EXPORTED: Final = "EXP"
|
|
13
21
|
# An special owner used for exported (AKA public) file/folders
|
|
14
22
|
|
|
15
23
|
|
|
24
|
+
class KeyLookup(NamedTuple):
|
|
25
|
+
full_key: ComposedFileKey | NodeKey
|
|
26
|
+
share_key: NodeKey | None
|
|
27
|
+
|
|
28
|
+
|
|
16
29
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
17
30
|
class MegaVault:
|
|
18
31
|
master_key: tuple[int, ...] = ()
|
|
@@ -35,7 +48,7 @@ class MegaVault:
|
|
|
35
48
|
if key := new_keys.get(node_id):
|
|
36
49
|
self._shared_keys.setdefault(owner, {})[node_id] = key
|
|
37
50
|
|
|
38
|
-
def __getitem__(self, node: Node) ->
|
|
51
|
+
def __getitem__(self, node: Node) -> KeyLookup:
|
|
39
52
|
share_key: tuple[int, ...] | None = None
|
|
40
53
|
|
|
41
54
|
# my files/folders
|
|
@@ -63,7 +76,10 @@ class MegaVault:
|
|
|
63
76
|
else:
|
|
64
77
|
raise RuntimeError(f"We do not have keys for {node = }")
|
|
65
78
|
|
|
66
|
-
|
|
79
|
+
if len(full_key) not in (4, 8):
|
|
80
|
+
raise RuntimeError(f"Invalid key len found for node {node.id} ({len(full_key)})")
|
|
81
|
+
|
|
82
|
+
return KeyLookup(cast("ComposedFileKey | NodeKey", full_key), share_key)
|
|
67
83
|
|
|
68
84
|
def save_public_key(self, node_id: NodeID, share_key: tuple[int, ...]) -> None:
|
|
69
85
|
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
|