async-mega-py 2.5.1__tar.gz → 2.5.2__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 (26) hide show
  1. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/PKG-INFO +1 -1
  2. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/pyproject.toml +1 -1
  3. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/pyproject.toml.orig +1 -1
  4. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/core.py +14 -5
  5. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/data_structures.py +9 -7
  6. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/errors.py +7 -1
  7. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/filesystem.py +15 -7
  8. async_mega_py-2.5.2/src/mega/vault.py +103 -0
  9. async_mega_py-2.5.1/src/mega/vault.py +0 -85
  10. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/LICENSE +0 -0
  11. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/README.md +0 -0
  12. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/__init__.py +0 -0
  13. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/__main__.py +0 -0
  14. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/api.py +0 -0
  15. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/auth.py +0 -0
  16. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/chunker.py +0 -0
  17. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/client.py +0 -0
  18. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/crypto.py +0 -0
  19. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/download.py +0 -0
  20. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/env.py +0 -0
  21. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/progress/__init__.py +0 -0
  22. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/progress/rich_progress.py +0 -0
  23. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/py.typed +0 -0
  24. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/transfer_it.py +0 -0
  25. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/upload.py +0 -0
  26. {async_mega_py-2.5.1 → async_mega_py-2.5.2}/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.5.1
3
+ Version: 2.5.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "async-mega.py"
3
- version = "2.5.1"
3
+ version = "2.5.2"
4
4
  classifiers = [
5
5
  "Development Status :: 5 - Production/Stable",
6
6
  "Environment :: Console",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "async-mega.py"
3
- version = "2.5.1"
3
+ version = "2.5.2"
4
4
  authors = [{name = "NTFSvolume", email = "NTFSvolume@proton.me"}]
5
5
  classifiers = [
6
6
  "Development Status :: 5 - Production/Stable",
@@ -23,7 +23,6 @@ from mega.crypto import (
23
23
  encrypt_key,
24
24
  )
25
25
  from mega.data_structures import (
26
- _EMPTY_ATTRS,
27
26
  AccountStatsSerialized,
28
27
  Attributes,
29
28
  Crypto,
@@ -235,9 +234,19 @@ class MegaCore:
235
234
  def decrypt(self, node: Node) -> Node:
236
235
  crypto = attributes = None
237
236
  if node.type in (NodeType.FILE, NodeType.FOLDER):
238
- full_key, share_key = self.vault[node]
239
- crypto = Crypto.decompose(full_key, node.type, share_key)
240
- attributes = self.decrypt_attrs(node._a, crypto.key, node.id)
237
+ possible_keys = self.vault.get_keys(node)
238
+ if len(possible_keys) > 1:
239
+ logger.warning(
240
+ "Found multiple keys for node %s (%s keys), trying all of them", node.id, len(possible_keys)
241
+ )
242
+
243
+ for key in possible_keys:
244
+ crypto = Crypto.decompose(key.full_key, node.type, key.share_key)
245
+ attributes = self._decrypt_attrs(node._a, crypto.key)
246
+ if not attributes.is_error():
247
+ break
248
+ else:
249
+ logger.warning("Unable to decrypt attributes of node %s", node.id)
241
250
 
242
251
  else:
243
252
  name = {
@@ -419,7 +428,7 @@ class MegaCore:
419
428
  @classmethod
420
429
  def decrypt_attrs(cls, attrs: str, key: tuple[int, ...], node_id: NodeID) -> Attributes:
421
430
  attributes = cls._decrypt_attrs(attrs, key)
422
- if attributes is _EMPTY_ATTRS:
431
+ if attributes.is_error():
423
432
  logger.warning("Unable to decrypt attributes of node %s", node_id)
424
433
  return attributes
425
434
 
@@ -13,7 +13,7 @@ import dataclasses
13
13
  import time
14
14
  from enum import IntEnum
15
15
  from types import MappingProxyType
16
- from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, Self, TypeAlias, TypedDict
16
+ from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, Self, TypeAlias, TypedDict, final
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from collections.abc import Mapping
@@ -236,9 +236,6 @@ class Crypto(_DictDumper):
236
236
  )
237
237
  assert iv
238
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) = }")
242
239
 
243
240
  return Crypto(key, iv, meta_mac, composed_key, share_key) # pyright: ignore[reportArgumentType]
244
241
 
@@ -329,17 +326,22 @@ class Node(_DictDumper):
329
326
  _LABELS: Final = "", "red", "orange", "yellow", "green", "blue", "purple", "grey"
330
327
 
331
328
 
329
+ @final
332
330
  @dataclasses.dataclass(slots=True, frozen=True)
333
331
  class Attributes(_DictDumper):
334
332
  name: str
335
333
  label: str = ""
336
334
  favorited: bool = False
337
335
 
338
- @classmethod
339
- def parse(cls, attrs: AttributesSerialized) -> Self:
336
+ def is_error(self) -> bool:
337
+ return self is _EMPTY_ATTRS
338
+
339
+ @staticmethod
340
+ def parse(attrs: AttributesSerialized) -> Attributes:
340
341
  if not attrs:
341
342
  return _EMPTY_ATTRS
342
- return cls(
343
+
344
+ return Attributes(
343
345
  name=attrs.get("n", ""),
344
346
  label=_LABELS[attrs.get("lbl", 0)],
345
347
  favorited=bool(attrs.get("fav")),
@@ -3,12 +3,18 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Final
4
4
 
5
5
  if TYPE_CHECKING:
6
- from mega.data_structures import NodeID
6
+ from mega.data_structures import Node, NodeID
7
7
 
8
8
 
9
9
  class MegaNzError(Exception): ...
10
10
 
11
11
 
12
+ class FileSystemError(MegaNzError, RuntimeError):
13
+ def __init__(self, nodes: tuple[Node, ...]) -> None:
14
+ super().__init__("Unable to decript attribute of some nodes in the file system")
15
+ self.nodes = nodes
16
+
17
+
12
18
  class ValidationError(MegaNzError, ValueError):
13
19
  """Error in validation stage"""
14
20
 
@@ -12,7 +12,7 @@ from types import MappingProxyType
12
12
  from typing import TYPE_CHECKING, Any, Final, NamedTuple, Self
13
13
 
14
14
  from mega.data_structures import Node, NodeID, NodeType, _DictDumper
15
- from mega.errors import MultipleNodesFoundError, ValidationError
15
+ from mega.errors import FileSystemError, MultipleNodesFoundError, ValidationError
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from collections.abc import Generator, Iterable, Iterator
@@ -115,7 +115,7 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
115
115
  yield from self.iterdir(self.trash_bin.id)
116
116
 
117
117
  @classmethod
118
- def build(cls, nodes: Iterable[Node]) -> Self:
118
+ def build(cls, nodes: Iterable[Node], *, strict: bool = False) -> Self:
119
119
  root = inbox = trash_bin = None
120
120
  file_count = folder_count = 0
121
121
 
@@ -127,7 +127,12 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
127
127
  ids = current.id, found.id
128
128
  raise ValidationError(f"Multiple nodes of type {found.type.name} found: {ids}")
129
129
 
130
+ decrypt_errors: list[Node] = []
130
131
  for node in nodes:
132
+ if node.attributes.is_error() and strict:
133
+ decrypt_errors.append(node)
134
+ continue
135
+
131
136
  nodes_map[node.id] = node
132
137
  if node.parent_id:
133
138
  children.setdefault(node.parent_id, []).append(node.id)
@@ -148,6 +153,9 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
148
153
  case _:
149
154
  raise RuntimeError # pyright: ignore[reportUnreachable]
150
155
 
156
+ if decrypt_errors:
157
+ raise FileSystemError(nodes=tuple(decrypt_errors))
158
+
151
159
  return cls(
152
160
  root=root,
153
161
  inbox=inbox,
@@ -170,8 +178,8 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
170
178
  )
171
179
 
172
180
  @classmethod
173
- def from_dump(cls, dump: dict[str, Any], /) -> Self:
174
- return cls.build(Node.from_dump(node) for node in dump["nodes"].values())
181
+ def from_dump(cls, dump: dict[str, Any], /, *, strict: bool = False) -> Self:
182
+ return cls.build((Node.from_dump(node) for node in dump["nodes"].values()), strict=strict)
175
183
 
176
184
 
177
185
  @dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True, repr=False)
@@ -193,7 +201,7 @@ class FileSystem(SimpleFileSystem):
193
201
  _deleted: frozenset[NodeID]
194
202
 
195
203
  @classmethod
196
- def build(cls, nodes: Iterable[Node]) -> Self:
204
+ def build(cls, nodes: Iterable[Node], *, strict: bool = False) -> Self:
197
205
  # This is really expensive
198
206
  # We do 5 loops over all nodes:
199
207
  # SimpleFileSystem:
@@ -205,14 +213,14 @@ class FileSystem(SimpleFileSystem):
205
213
  # - 4. Sort their paths
206
214
  # - 5. Freeze inv paths (list -> tuple)
207
215
 
208
- self = SimpleFileSystem.build(nodes)
216
+ self = SimpleFileSystem.build(nodes, strict=strict)
209
217
 
210
218
  roots = list(filter(None, (self.root, self.inbox, self.trash_bin))) or [next(iter(self.nodes.values()))]
211
219
  paths: dict[NodeID, PurePosixPath] = {}
212
220
  inv_paths: dict[PurePosixPath, list[NodeID]] = {}
213
221
  deleted_ids: set[NodeID] = set()
214
222
 
215
- for node_id, path, was_deleted in sorted(_resolve_paths(self, *roots), key=lambda x: str(x[1]).casefold()):
223
+ for node_id, path, was_deleted in sorted(_resolve_paths(self, *roots), key=lambda x: str(x.path).casefold()):
216
224
  paths[node_id] = path
217
225
  inv_paths.setdefault(path, []).append(node_id)
218
226
  if was_deleted:
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import logging
5
+ from typing import TYPE_CHECKING, Final, NamedTuple
6
+
7
+ from mega.crypto import b64_to_a32, decrypt_key
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Generator
11
+
12
+ from mega.data_structures import ComposedFileKey, GetNodesResponse, Node, NodeID, NodeKey, SharedKeys, UserID
13
+
14
+ logger = logging.getLogger(__name__)
15
+ _EXPORTED: Final = "EXP"
16
+ # An special owner used for exported (AKA public) file/folders
17
+
18
+
19
+ class KeyLookup(NamedTuple):
20
+ full_key: ComposedFileKey | NodeKey
21
+ share_key: NodeKey | None
22
+
23
+
24
+ @dataclasses.dataclass(slots=True, frozen=True)
25
+ class MegaVault:
26
+ master_key: tuple[int, ...] = ()
27
+
28
+ _shared_keys: dict[UserID, SharedKeys] = dataclasses.field(default_factory=dict)
29
+
30
+ def __post_init__(self) -> None:
31
+ _ = self._shared_keys.setdefault(_EXPORTED, {})
32
+
33
+ def update(self, api_resp: GetNodesResponse) -> None:
34
+ """Parse and update keys not associated with an user"""
35
+
36
+ new_keys: SharedKeys = {}
37
+ for share_key in api_resp["ok"]:
38
+ node_id, key = share_key["h"], share_key["k"]
39
+ new_keys[node_id] = decrypt_key(b64_to_a32(key), self.master_key)
40
+
41
+ for share_target in api_resp["s"]:
42
+ node_id, owner = share_target["h"], share_target["u"]
43
+ if key := new_keys.get(node_id):
44
+ self._shared_keys.setdefault(owner, {})[node_id] = key
45
+
46
+ def __getitem__(self, node: Node) -> KeyLookup:
47
+ keys = self.get_keys(node)
48
+ if len(keys) > 1:
49
+ logger.warning("Found multiple keys for node %s, using latest one", node.id)
50
+ return keys[-1]
51
+
52
+ def _get_keys(self, node: Node) -> Generator[KeyLookup]:
53
+ def cast_key(full_key: tuple[int, ...], share_key: tuple[int, ...] | None):
54
+ if len(full_key) not in (4, 8):
55
+ raise RuntimeError(f"Invalid key len found for node {node.id} ({len(full_key)})")
56
+
57
+ return KeyLookup(full_key, share_key) # pyright: ignore[reportArgumentType]
58
+
59
+ # my files/folders
60
+ if node.owner in node.keys:
61
+ full_key = decrypt_key(b64_to_a32(node.keys[node.owner]), self.master_key)
62
+ yield cast_key(full_key, None)
63
+ return
64
+
65
+ # folders shared with me
66
+ if node.share_owner and node.share_key and node.id in node.keys:
67
+ share_key = decrypt_key(b64_to_a32(node.share_key), self.master_key)
68
+ full_key = decrypt_key(b64_to_a32(node.keys[node.id]), share_key)
69
+ self._shared_keys.setdefault(node.share_owner, {})[node.id] = share_key
70
+ yield cast_key(full_key, share_key)
71
+ return
72
+
73
+ # files shared with me
74
+ if node.owner in self._shared_keys:
75
+ shared_keys = self._shared_keys[node.owner]
76
+ for node_id, encrypted_key in node.keys.items():
77
+ try:
78
+ share_key = shared_keys[node_id]
79
+ except KeyError:
80
+ continue
81
+
82
+ full_key = decrypt_key(b64_to_a32(encrypted_key), share_key)
83
+ yield cast_key(full_key, share_key)
84
+ return
85
+
86
+ # public files/folders
87
+ if share_key := self._shared_keys[_EXPORTED].get(node.id):
88
+ exported_nodes = self._shared_keys[_EXPORTED].keys()
89
+ for node_id, encrypted_key in node.keys.items():
90
+ if node_id not in exported_nodes:
91
+ continue
92
+
93
+ full_key = decrypt_key(b64_to_a32(encrypted_key), share_key)
94
+ yield cast_key(full_key, share_key)
95
+
96
+ def get_keys(self, node: Node) -> list[KeyLookup]:
97
+ keys = list(self._get_keys(node))
98
+ if len(keys) == 0:
99
+ raise RuntimeError(f"We do not have keys for {node = }")
100
+ return keys
101
+
102
+ def save_public_key(self, node_id: NodeID, share_key: tuple[int, ...]) -> None:
103
+ self._shared_keys[_EXPORTED][node_id] = share_key
@@ -1,85 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import dataclasses
4
- from typing import TYPE_CHECKING, Final, NamedTuple, cast
5
-
6
- from mega.crypto import b64_to_a32, decrypt_key
7
-
8
- if TYPE_CHECKING:
9
- from mega.data_structures import (
10
- ComposedFileKey,
11
- GetNodesResponse,
12
- Node,
13
- NodeID,
14
- NodeKey,
15
- SharedKeys,
16
- UserID,
17
- )
18
-
19
-
20
- _EXPORTED: Final = "EXP"
21
- # An special owner used for exported (AKA public) file/folders
22
-
23
-
24
- class KeyLookup(NamedTuple):
25
- full_key: ComposedFileKey | NodeKey
26
- share_key: NodeKey | None
27
-
28
-
29
- @dataclasses.dataclass(slots=True, frozen=True)
30
- class MegaVault:
31
- master_key: tuple[int, ...] = ()
32
-
33
- _shared_keys: dict[UserID, SharedKeys] = dataclasses.field(default_factory=dict)
34
-
35
- def __post_init__(self) -> None:
36
- _ = self._shared_keys.setdefault(_EXPORTED, {})
37
-
38
- def update(self, api_resp: GetNodesResponse) -> None:
39
- """Parse and update keys not associated with an user"""
40
-
41
- new_keys: SharedKeys = {}
42
- for share_key in api_resp["ok"]:
43
- node_id, key = share_key["h"], share_key["k"]
44
- new_keys[node_id] = decrypt_key(b64_to_a32(key), self.master_key)
45
-
46
- for share_target in api_resp["s"]:
47
- node_id, owner = share_target["h"], share_target["u"]
48
- if key := new_keys.get(node_id):
49
- self._shared_keys.setdefault(owner, {})[node_id] = key
50
-
51
- def __getitem__(self, node: Node) -> KeyLookup:
52
- share_key: tuple[int, ...] | None = None
53
-
54
- # my files/folders
55
- if node.owner in node.keys:
56
- full_key = decrypt_key(b64_to_a32(node.keys[node.owner]), self.master_key)
57
-
58
- # folders shared with me
59
- elif node.share_owner and node.share_key and node.id in node.keys:
60
- share_key = decrypt_key(b64_to_a32(node.share_key), self.master_key)
61
- full_key = decrypt_key(b64_to_a32(node.keys[node.id]), share_key)
62
- self._shared_keys.setdefault(node.share_owner, {})[node.id] = share_key
63
-
64
- # files shared with me
65
- elif node.owner in self._shared_keys:
66
- real_node_id = next(node_id for node_id in node.keys if node_id in self._shared_keys[node.owner])
67
- share_key = self._shared_keys[node.owner][real_node_id]
68
- full_key = decrypt_key(b64_to_a32(node.keys[real_node_id]), share_key)
69
-
70
- # public files/folders
71
- elif share_key := self._shared_keys[_EXPORTED].get(node.id):
72
- real_node_id = next(node_id for node_id in node.keys if node_id in self._shared_keys[_EXPORTED])
73
- encrypted_key = b64_to_a32(node.keys[real_node_id])
74
- full_key = decrypt_key(encrypted_key, share_key)
75
-
76
- else:
77
- raise RuntimeError(f"We do not have keys for {node = }")
78
-
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)
83
-
84
- def save_public_key(self, node_id: NodeID, share_key: tuple[int, ...]) -> None:
85
- self._shared_keys[_EXPORTED][node_id] = share_key
File without changes
File without changes