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.
Files changed (24) hide show
  1. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/PKG-INFO +1 -1
  2. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/pyproject.toml +1 -1
  3. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/__main__.py +1 -1
  4. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/client.py +2 -2
  5. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/core.py +13 -4
  6. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/data_structures.py +47 -19
  7. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/vault.py +20 -4
  8. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/LICENSE +0 -0
  9. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/README.md +0 -0
  10. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/__init__.py +0 -0
  11. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/api.py +0 -0
  12. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/auth.py +0 -0
  13. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/chunker.py +0 -0
  14. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/crypto.py +0 -0
  15. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/download.py +0 -0
  16. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/env.py +0 -0
  17. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/errors.py +0 -0
  18. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/filesystem.py +0 -0
  19. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/progress/__init__.py +0 -0
  20. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/progress/rich_progress.py +0 -0
  21. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/py.typed +0 -0
  22. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/transfer_it.py +0 -0
  23. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/upload.py +0 -0
  24. {async_mega_py-2.2.2 → async_mega_py-2.3.1}/src/mega/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-mega.py
3
- Version: 2.2.2
3
+ Version: 2.3.1
4
4
  Summary: Python library for the Mega.nz and Transfer.it API
5
5
  Keywords: api,downloader,mega,mega.nz,transfer.it
6
6
  Author: NTFSvolume
@@ -34,7 +34,7 @@ license = "Apache-2.0"
34
34
  license-files = ["LICENSE"]
35
35
  readme = "README.md"
36
36
  requires-python = ">=3.11"
37
- version = "2.2.2"
37
+ version = "2.3.1"
38
38
 
39
39
  [project.optional-dependencies]
40
40
  cli = [
@@ -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 decrypt_attrs(attrs: str, key: tuple[int, ...]) -> Attributes:
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, 5]] # Node type
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: tuple[int, int, int, int]
180
+ key: NodeKey
178
181
  iv: tuple[int, int]
179
182
  meta_mac: tuple[int, int]
180
183
 
181
- full_key: tuple[int, int, int, int, int, int, int, int]
182
- share_key: tuple[int, ...] | None
184
+ full_key: ComposedFileKey | NodeKey
185
+ share_key: NodeKey | None
183
186
 
184
187
  @classmethod
185
188
  def compose(
186
189
  cls,
187
- key: tuple[int, ...],
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
- full_key = (
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
- full_key = *key, *iv, *meta_mac
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, full_key, None) # pyright: ignore[reportArgumentType]
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
- full_key: tuple[int, ...],
219
+ composed_key: ComposedFileKey | NodeKey,
210
220
  node_type: NodeType = NodeType.FILE,
211
- share_key: tuple[int, ...] | None = None,
221
+ share_key: NodeKey | None = None,
212
222
  ) -> Crypto:
213
- iv = full_key[4:6]
214
- meta_mac = full_key[6:8]
215
- key = full_key
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, full_key, share_key) # pyright: ignore[reportArgumentType]
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 GetNodesResponse, Node, NodeID, SharedKeys, UserID
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) -> tuple[tuple[int, ...], tuple[int, ...] | None]:
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
- return full_key, share_key
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