async-mega-py 2.2.1__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.1 → async_mega_py-2.3.0}/PKG-INFO +25 -26
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/README.md +24 -25
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/pyproject.toml +1 -1
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/__main__.py +1 -1
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/client.py +2 -2
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/core.py +13 -4
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/data_structures.py +52 -20
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/vault.py +19 -6
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/LICENSE +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/__init__.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/api.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/auth.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/chunker.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/crypto.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/download.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/env.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/errors.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/filesystem.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/progress/__init__.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/progress/rich_progress.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/py.typed +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/transfer_it.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/src/mega/upload.py +0 -0
- {async_mega_py-2.2.1 → async_mega_py-2.3.0}/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.
|
|
3
|
+
Version: 2.3.0
|
|
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
|
|
@@ -79,7 +79,7 @@ Please check [`src/mega/data_structures.py`](src/mega/data_structures.py) for de
|
|
|
79
79
|
> [!TIP]
|
|
80
80
|
> You can run the commands bellow in an interactive python (the asyncio REPL). Try them in real time as is, using `async/await` keywords!
|
|
81
81
|
>
|
|
82
|
-
>
|
|
82
|
+
> `uv run -p3.12 --with async-mega-py python -m asyncio`
|
|
83
83
|
|
|
84
84
|
```python
|
|
85
85
|
from mega.client import MegaNzClient
|
|
@@ -99,7 +99,7 @@ from mega.client import MegaNzClient
|
|
|
99
99
|
# Also works without using it as a context manager, but you have the responsability to close the session at the end
|
|
100
100
|
mega = MegaNzClient()
|
|
101
101
|
await mega.login() # login using a temporary anonymous account
|
|
102
|
-
# await mega.close()
|
|
102
|
+
# await mega.close()
|
|
103
103
|
|
|
104
104
|
```
|
|
105
105
|
|
|
@@ -163,7 +163,7 @@ await mega.download(uploaded_file, output_dir)
|
|
|
163
163
|
|
|
164
164
|
# Download a public file
|
|
165
165
|
file_url = "https://mega.nz/file/vJ9EBJBC#vA1XpbngXcnN7lqXEWGQFPDc5ZlG6yltCHwGN-0O2LQ"
|
|
166
|
-
public_handle, public_key = mega.parse_file_url(file_url)
|
|
166
|
+
public_handle, public_key = mega.parse_file_url(file_url)
|
|
167
167
|
await mega.download_public_file(public_handle, public_key, output_dir)
|
|
168
168
|
|
|
169
169
|
# Download a public folder
|
|
@@ -186,7 +186,7 @@ print (result.is_folder)
|
|
|
186
186
|
> You can show a progress bar on the terminal for each download/upload by calling them within the `progress_bar` context manager (needs optional dependency `rich` to be installed):
|
|
187
187
|
|
|
188
188
|
```python
|
|
189
|
-
with mega.progress_bar:
|
|
189
|
+
with mega.progress_bar:
|
|
190
190
|
results = await mega.download_public_folder(public_handle, public_key, output_dir / "with_bar")
|
|
191
191
|
```
|
|
192
192
|
|
|
@@ -264,16 +264,14 @@ The output will be:
|
|
|
264
264
|
|
|
265
265
|
```json
|
|
266
266
|
{
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
"oImwb6nN": "/tests/script.js",
|
|
268
|
+
"FIXitv4F": "/tests/scripts",
|
|
269
|
+
"0fPFklV3": "/tests/scripts/notes.txt",
|
|
270
|
+
"l9zkz1GU": "/tests/scripts/script.js",
|
|
271
|
+
"E4LqT4EF": "/tests/scripts/styles.css"
|
|
272
272
|
}
|
|
273
273
|
```
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
277
275
|
```python
|
|
278
276
|
# Get the path to a node
|
|
279
277
|
fs.absolute_path("0fPFklV3") # /tests/scripts/notes.txt
|
|
@@ -294,24 +292,25 @@ for node in fs.iterdir(folder.id, recursive=True):
|
|
|
294
292
|
print(fs.absolute_path(node.id))
|
|
295
293
|
|
|
296
294
|
```
|
|
295
|
+
|
|
297
296
|
Output will be:
|
|
298
297
|
|
|
299
298
|
```json
|
|
300
299
|
[
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
300
|
+
"/tests/logo.png",
|
|
301
|
+
"/tests/logo.png",
|
|
302
|
+
"/tests/script.js",
|
|
303
|
+
"/tests/scripts",
|
|
304
|
+
"/tests/scripts/notes.txt",
|
|
305
|
+
"/tests/scripts/script.js",
|
|
306
|
+
"/tests/scripts/styles.css",
|
|
307
|
+
"/tests/setup.sh",
|
|
308
|
+
"/tests/utils.py"
|
|
310
309
|
]
|
|
311
310
|
```
|
|
312
311
|
|
|
313
312
|
> [!IMPORTANT]
|
|
314
|
-
> Mega's filesystem is
|
|
313
|
+
> Mega's filesystem is _not_ POSIX-compliant: multiple nodes may have the same path.
|
|
315
314
|
>
|
|
316
315
|
> If 2 nodes have the same path, `find` will throw an error.
|
|
317
316
|
|
|
@@ -352,10 +351,10 @@ mega-py --help
|
|
|
352
351
|
```
|
|
353
352
|
|
|
354
353
|
```powershell
|
|
355
|
-
Usage: mega-py [OPTIONS] COMMAND [ARGS]...
|
|
354
|
+
Usage: mega-py [OPTIONS] COMMAND [ARGS]...
|
|
356
355
|
|
|
357
|
-
CLI app for the Mega.nz and Transfer.it. Set
|
|
358
|
-
enviroment variables to use them as credentials for Mega
|
|
356
|
+
CLI app for the Mega.nz and Transfer.it. Set MEGA_EMAIL and MEGA_PWD
|
|
357
|
+
enviroment variables to use them as credentials for Mega
|
|
359
358
|
|
|
360
359
|
╭─ Options ──────────────────────────────────────────────────────────────────────╮
|
|
361
360
|
│ --verbose -v Increase verbosity (-v shows debug logs, -vv │
|
|
@@ -371,6 +370,6 @@ Usage: mega-py [OPTIONS] COMMAND [ARGS]...
|
|
|
371
370
|
```
|
|
372
371
|
|
|
373
372
|
> [!TIP]
|
|
374
|
-
> The CLI app does
|
|
373
|
+
> The CLI app does _not_ accept login credentials, but you can still use your account by setting up the `MEGA_EMAIL` and `MEGA_PWD` enviroment variables
|
|
375
374
|
>
|
|
376
375
|
> It will also read them from an `.env` file (if found)
|
|
@@ -46,7 +46,7 @@ Please check [`src/mega/data_structures.py`](src/mega/data_structures.py) for de
|
|
|
46
46
|
> [!TIP]
|
|
47
47
|
> You can run the commands bellow in an interactive python (the asyncio REPL). Try them in real time as is, using `async/await` keywords!
|
|
48
48
|
>
|
|
49
|
-
>
|
|
49
|
+
> `uv run -p3.12 --with async-mega-py python -m asyncio`
|
|
50
50
|
|
|
51
51
|
```python
|
|
52
52
|
from mega.client import MegaNzClient
|
|
@@ -66,7 +66,7 @@ from mega.client import MegaNzClient
|
|
|
66
66
|
# Also works without using it as a context manager, but you have the responsability to close the session at the end
|
|
67
67
|
mega = MegaNzClient()
|
|
68
68
|
await mega.login() # login using a temporary anonymous account
|
|
69
|
-
# await mega.close()
|
|
69
|
+
# await mega.close()
|
|
70
70
|
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -130,7 +130,7 @@ await mega.download(uploaded_file, output_dir)
|
|
|
130
130
|
|
|
131
131
|
# Download a public file
|
|
132
132
|
file_url = "https://mega.nz/file/vJ9EBJBC#vA1XpbngXcnN7lqXEWGQFPDc5ZlG6yltCHwGN-0O2LQ"
|
|
133
|
-
public_handle, public_key = mega.parse_file_url(file_url)
|
|
133
|
+
public_handle, public_key = mega.parse_file_url(file_url)
|
|
134
134
|
await mega.download_public_file(public_handle, public_key, output_dir)
|
|
135
135
|
|
|
136
136
|
# Download a public folder
|
|
@@ -153,7 +153,7 @@ print (result.is_folder)
|
|
|
153
153
|
> You can show a progress bar on the terminal for each download/upload by calling them within the `progress_bar` context manager (needs optional dependency `rich` to be installed):
|
|
154
154
|
|
|
155
155
|
```python
|
|
156
|
-
with mega.progress_bar:
|
|
156
|
+
with mega.progress_bar:
|
|
157
157
|
results = await mega.download_public_folder(public_handle, public_key, output_dir / "with_bar")
|
|
158
158
|
```
|
|
159
159
|
|
|
@@ -231,16 +231,14 @@ The output will be:
|
|
|
231
231
|
|
|
232
232
|
```json
|
|
233
233
|
{
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
234
|
+
"oImwb6nN": "/tests/script.js",
|
|
235
|
+
"FIXitv4F": "/tests/scripts",
|
|
236
|
+
"0fPFklV3": "/tests/scripts/notes.txt",
|
|
237
|
+
"l9zkz1GU": "/tests/scripts/script.js",
|
|
238
|
+
"E4LqT4EF": "/tests/scripts/styles.css"
|
|
239
239
|
}
|
|
240
240
|
```
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
|
|
244
242
|
```python
|
|
245
243
|
# Get the path to a node
|
|
246
244
|
fs.absolute_path("0fPFklV3") # /tests/scripts/notes.txt
|
|
@@ -261,24 +259,25 @@ for node in fs.iterdir(folder.id, recursive=True):
|
|
|
261
259
|
print(fs.absolute_path(node.id))
|
|
262
260
|
|
|
263
261
|
```
|
|
262
|
+
|
|
264
263
|
Output will be:
|
|
265
264
|
|
|
266
265
|
```json
|
|
267
266
|
[
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
267
|
+
"/tests/logo.png",
|
|
268
|
+
"/tests/logo.png",
|
|
269
|
+
"/tests/script.js",
|
|
270
|
+
"/tests/scripts",
|
|
271
|
+
"/tests/scripts/notes.txt",
|
|
272
|
+
"/tests/scripts/script.js",
|
|
273
|
+
"/tests/scripts/styles.css",
|
|
274
|
+
"/tests/setup.sh",
|
|
275
|
+
"/tests/utils.py"
|
|
277
276
|
]
|
|
278
277
|
```
|
|
279
278
|
|
|
280
279
|
> [!IMPORTANT]
|
|
281
|
-
> Mega's filesystem is
|
|
280
|
+
> Mega's filesystem is _not_ POSIX-compliant: multiple nodes may have the same path.
|
|
282
281
|
>
|
|
283
282
|
> If 2 nodes have the same path, `find` will throw an error.
|
|
284
283
|
|
|
@@ -319,10 +318,10 @@ mega-py --help
|
|
|
319
318
|
```
|
|
320
319
|
|
|
321
320
|
```powershell
|
|
322
|
-
Usage: mega-py [OPTIONS] COMMAND [ARGS]...
|
|
321
|
+
Usage: mega-py [OPTIONS] COMMAND [ARGS]...
|
|
323
322
|
|
|
324
|
-
CLI app for the Mega.nz and Transfer.it. Set
|
|
325
|
-
enviroment variables to use them as credentials for Mega
|
|
323
|
+
CLI app for the Mega.nz and Transfer.it. Set MEGA_EMAIL and MEGA_PWD
|
|
324
|
+
enviroment variables to use them as credentials for Mega
|
|
326
325
|
|
|
327
326
|
╭─ Options ──────────────────────────────────────────────────────────────────────╮
|
|
328
327
|
│ --verbose -v Increase verbosity (-v shows debug logs, -vv │
|
|
@@ -338,6 +337,6 @@ Usage: mega-py [OPTIONS] COMMAND [ARGS]...
|
|
|
338
337
|
```
|
|
339
338
|
|
|
340
339
|
> [!TIP]
|
|
341
|
-
> The CLI app does
|
|
340
|
+
> The CLI app does _not_ accept login credentials, but you can still use your account by setting up the `MEGA_EMAIL` and `MEGA_PWD` enviroment variables
|
|
342
341
|
>
|
|
343
342
|
> It will also read them from an `.env` file (if found)
|
|
@@ -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
|
|
|
@@ -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, Self, TypeAlias, TypedDict
|
|
16
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, Self, TypeAlias, TypedDict
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from collections.abc import Mapping
|
|
@@ -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[
|
|
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
|
|
|
@@ -76,6 +80,7 @@ class NodeSerialized(TypedDict):
|
|
|
76
80
|
sk: NotRequired[str] # Share key, only present present in shared (public) files / folder
|
|
77
81
|
|
|
78
82
|
s: NotRequired[int] # size (files only)
|
|
83
|
+
fa: NotRequired[str] # additional file attributes for multimedia files (ex: thumbail)
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
class OwnerKeys(TypedDict):
|
|
@@ -173,23 +178,23 @@ class FileInfo(_DictDumper):
|
|
|
173
178
|
|
|
174
179
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
175
180
|
class Crypto(_DictDumper):
|
|
176
|
-
key:
|
|
181
|
+
key: NodeKey
|
|
177
182
|
iv: tuple[int, int]
|
|
178
183
|
meta_mac: tuple[int, int]
|
|
179
184
|
|
|
180
|
-
full_key:
|
|
181
|
-
share_key:
|
|
185
|
+
full_key: ComposedFileKey | ComposedFolderKey
|
|
186
|
+
share_key: NodeKey | None
|
|
182
187
|
|
|
183
188
|
@classmethod
|
|
184
189
|
def compose(
|
|
185
190
|
cls,
|
|
186
|
-
key:
|
|
187
|
-
iv: tuple[int,
|
|
188
|
-
meta_mac: tuple[int,
|
|
191
|
+
key: NodeKey,
|
|
192
|
+
iv: tuple[int, int],
|
|
193
|
+
meta_mac: tuple[int, int],
|
|
189
194
|
node_type: NodeType = NodeType.FILE,
|
|
190
195
|
) -> Crypto:
|
|
191
196
|
if node_type is NodeType.FILE:
|
|
192
|
-
|
|
197
|
+
composed_key = (
|
|
193
198
|
key[0] ^ iv[0],
|
|
194
199
|
key[1] ^ iv[1],
|
|
195
200
|
key[2] ^ meta_mac[0],
|
|
@@ -197,30 +202,47 @@ class Crypto(_DictDumper):
|
|
|
197
202
|
*iv,
|
|
198
203
|
*meta_mac,
|
|
199
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
|
+
|
|
200
208
|
else:
|
|
201
|
-
|
|
209
|
+
if len(key) != 4:
|
|
210
|
+
raise RuntimeError(f"Invalid key, expected key len for folders is 4, got {len(key) = }")
|
|
202
211
|
|
|
203
|
-
|
|
212
|
+
iv = ()
|
|
213
|
+
meta_mac = ()
|
|
214
|
+
composed_key = key
|
|
215
|
+
|
|
216
|
+
return Crypto(key, iv, meta_mac, composed_key, None) # pyright: ignore[reportArgumentType]
|
|
204
217
|
|
|
205
218
|
@classmethod
|
|
206
219
|
def decompose(
|
|
207
220
|
cls,
|
|
208
|
-
|
|
221
|
+
composed_key: ComposedFileKey | ComposedFolderKey,
|
|
209
222
|
node_type: NodeType = NodeType.FILE,
|
|
210
|
-
share_key:
|
|
223
|
+
share_key: NodeKey | None = None,
|
|
211
224
|
) -> Crypto:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
225
|
+
|
|
226
|
+
key = composed_key[:4]
|
|
227
|
+
iv = composed_key[4:6]
|
|
228
|
+
meta_mac = composed_key[6:8]
|
|
215
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
|
+
|
|
216
233
|
key = (
|
|
217
234
|
key[0] ^ iv[0],
|
|
218
235
|
key[1] ^ iv[1],
|
|
219
236
|
key[2] ^ meta_mac[0],
|
|
220
237
|
key[3] ^ meta_mac[1],
|
|
221
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) = }")
|
|
222
244
|
|
|
223
|
-
return Crypto(key, iv, meta_mac,
|
|
245
|
+
return Crypto(key, iv, meta_mac, composed_key, share_key) # pyright: ignore[reportArgumentType]
|
|
224
246
|
|
|
225
247
|
@classmethod
|
|
226
248
|
def from_dump(cls, dump: dict[str, Any]) -> Self:
|
|
@@ -267,6 +289,7 @@ class Node(_DictDumper):
|
|
|
267
289
|
keys: dict[str, str] = {}
|
|
268
290
|
|
|
269
291
|
size = node.get("s")
|
|
292
|
+
|
|
270
293
|
return Node(
|
|
271
294
|
id=node["h"],
|
|
272
295
|
parent_id=node["p"],
|
|
@@ -316,6 +339,8 @@ class Attributes(_DictDumper):
|
|
|
316
339
|
|
|
317
340
|
@classmethod
|
|
318
341
|
def parse(cls, attrs: AttributesSerialized) -> Self:
|
|
342
|
+
if not attrs:
|
|
343
|
+
return _EMPTY_ATTRS
|
|
319
344
|
return cls(
|
|
320
345
|
name=attrs.get("n", ""),
|
|
321
346
|
label=_LABELS[attrs.get("lbl", 0)],
|
|
@@ -334,6 +359,9 @@ class Attributes(_DictDumper):
|
|
|
334
359
|
}
|
|
335
360
|
|
|
336
361
|
|
|
362
|
+
_EMPTY_ATTRS = Attributes("")
|
|
363
|
+
|
|
364
|
+
|
|
337
365
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
338
366
|
class AccountBalance(_DictDumper):
|
|
339
367
|
amount: float
|
|
@@ -375,6 +403,7 @@ class AccountStatsSerialized(TypedDict):
|
|
|
375
403
|
mxfer: NotRequired[int] # maximum transfer allowance
|
|
376
404
|
srvratio: float # Ratio of PRO transfer quota that is able to be served to others
|
|
377
405
|
suntil: NotRequired[TimeStamp] # Expiration time of the currently active plan
|
|
406
|
+
rtt: NotRequired[int] # ???
|
|
378
407
|
|
|
379
408
|
|
|
380
409
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
@@ -393,10 +422,13 @@ class StorageMetrics(_DictDumper):
|
|
|
393
422
|
class StorageQuota(_DictDumper):
|
|
394
423
|
used: ByteSize
|
|
395
424
|
max: ByteSize
|
|
396
|
-
|
|
397
|
-
percent: int
|
|
398
425
|
threshold: int
|
|
399
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
|
+
|
|
400
432
|
@property
|
|
401
433
|
def ratio(self) -> float:
|
|
402
434
|
return self.used / self.max
|
|
@@ -416,7 +448,6 @@ class StorageQuota(_DictDumper):
|
|
|
416
448
|
return cls(
|
|
417
449
|
used=used,
|
|
418
450
|
max=max,
|
|
419
|
-
percent=int(used / max * 100),
|
|
420
451
|
threshold=threshold // 100,
|
|
421
452
|
)
|
|
422
453
|
|
|
@@ -449,6 +480,7 @@ class AccountStats(_DictParser, _DictDumper):
|
|
|
449
480
|
@classmethod
|
|
450
481
|
def parse(cls, data: AccountStatsSerialized) -> Self: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
451
482
|
transfer = None
|
|
483
|
+
|
|
452
484
|
if max_transfer := data.get("mxfer"):
|
|
453
485
|
assert "caxfer" in data
|
|
454
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
|
|
@@ -50,18 +58,23 @@ class MegaVault:
|
|
|
50
58
|
|
|
51
59
|
# files shared with me
|
|
52
60
|
elif node.owner in self._shared_keys:
|
|
53
|
-
real_node_id
|
|
61
|
+
real_node_id = next(node_id for node_id in node.keys if node_id in self._shared_keys[node.owner])
|
|
62
|
+
share_key = self._shared_keys[node.owner][real_node_id]
|
|
54
63
|
full_key = decrypt_key(b64_to_a32(node.keys[real_node_id]), share_key)
|
|
55
64
|
|
|
56
65
|
# public files/folders
|
|
57
66
|
elif share_key := self._shared_keys[_EXPORTED].get(node.id):
|
|
58
|
-
|
|
67
|
+
real_node_id = next(node_id for node_id in node.keys if node_id in self._shared_keys[_EXPORTED])
|
|
68
|
+
encrypted_key = b64_to_a32(node.keys[real_node_id])
|
|
59
69
|
full_key = decrypt_key(encrypted_key, share_key)
|
|
60
70
|
|
|
61
71
|
else:
|
|
62
72
|
raise RuntimeError(f"We do not have keys for {node = }")
|
|
63
73
|
|
|
64
|
-
|
|
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
|
|
65
78
|
|
|
66
79
|
def save_public_key(self, node_id: NodeID, share_key: tuple[int, ...]) -> None:
|
|
67
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
|