commit
6daa2bdf3c
15 changed files with 649 additions and 766 deletions
86
HISTORY.md
Normal file
86
HISTORY.md
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
Release History
|
||||||
|
===============
|
||||||
|
|
||||||
|
1.0.8 (unreleased)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fixes find method returning the wrong file when more than one file
|
||||||
|
exists with that name.
|
||||||
|
- Handle new shared file URLS.
|
||||||
|
|
||||||
|
1.0.7 (2020-03-25)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fix login by calculating public RSA exponent instead of hardcoding.
|
||||||
|
|
||||||
|
1.0.6 (2020-02-03)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fixes RSA public exponent issue.
|
||||||
|
- Switches dependency pycrypto to pycryptodome.
|
||||||
|
|
||||||
|
1.0.5 (2019-11-18)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Increase the wait time in between failed API request retries.
|
||||||
|
|
||||||
|
1.0.4 (2019-11-18)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Increase the wait time in between failed API request retries.
|
||||||
|
|
||||||
|
1.0.3 (2019-11-12)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fixes broken `download` method.
|
||||||
|
- Changes `download` and `download_url` methods to return the path to
|
||||||
|
the downloaded file, previously returned `None`.
|
||||||
|
- Added LICENSE.
|
||||||
|
|
||||||
|
1.0.2 (2019-11-07)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Reverts, "Replace pycrypto dependency with pycryptodome" as breaks
|
||||||
|
login process.
|
||||||
|
|
||||||
|
1.0.1 (2019-11-06)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- When a request fails due to EAGAIN response, retry with exp backoff
|
||||||
|
up to 20 seconds.
|
||||||
|
- Adds logging, removes print statements.
|
||||||
|
- Replace pycrypto dependency with pycryptodome.
|
||||||
|
- Removes Python 2 specific code.
|
||||||
|
|
||||||
|
1.0.0 (2019-10-31)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Removes broken method `get_contacts()`.
|
||||||
|
- Adds support for login with a v2 Mega user account.
|
||||||
|
- Adds `export()` method to share a file or folder, returning public
|
||||||
|
share URL with key.
|
||||||
|
- Adds code, message attrs to RequestError exception, makes message in
|
||||||
|
raised exceptions include more details.
|
||||||
|
- Alters `create_folder()` to accept a path including multiple sub
|
||||||
|
directories, adds support to create them all (similar to 'mkdir -p'
|
||||||
|
on unix systems).
|
||||||
|
- Adds `exclude_deleted=True` optional arg to `find()` method, to
|
||||||
|
exclude deleted nodes from results.
|
||||||
|
|
||||||
|
0.9.20 (2019-10-17)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Python 3 bugfix to `upload` method.
|
||||||
|
|
||||||
|
0.9.19 (2019-10-16)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Python 3 support and bugfixes.
|
||||||
|
- Update packaging code.
|
||||||
|
- Added changelog.
|
||||||
|
|
||||||
|
0.9.18 (2013-07-04)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Unknown
|
||||||
|
|
79
HISTORY.rst
79
HISTORY.rst
|
@ -1,79 +0,0 @@
|
||||||
.. :changelog:
|
|
||||||
|
|
||||||
Release History
|
|
||||||
===============
|
|
||||||
|
|
||||||
1.0.7 (2020-03-25)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Fix login by calculating public RSA exponent instead of hardcoding.
|
|
||||||
|
|
||||||
|
|
||||||
1.0.6 (2020-02-03)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Fixes RSA public exponent issue.
|
|
||||||
- Switches dependency pycrypto to pycryptodome.
|
|
||||||
|
|
||||||
|
|
||||||
1.0.5 (2019-11-18)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Increase the wait time in between failed API request retries.
|
|
||||||
|
|
||||||
|
|
||||||
1.0.4 (2019-11-18)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Increase the wait time in between failed API request retries.
|
|
||||||
|
|
||||||
|
|
||||||
1.0.3 (2019-11-12)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Fixes broken ``download`` method.
|
|
||||||
- Changes ``download`` and ``download_url`` methods to return the path to the downloaded file, previously returned ``None``.
|
|
||||||
- Added LICENSE.
|
|
||||||
|
|
||||||
|
|
||||||
1.0.2 (2019-11-07)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Reverts, "Replace pycrypto dependency with pycryptodome" as breaks login process.
|
|
||||||
|
|
||||||
|
|
||||||
1.0.1 (2019-11-06)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- When a request fails due to EAGAIN response, retry with exp backoff up to 20 seconds.
|
|
||||||
- Adds logging, removes print statements.
|
|
||||||
- Replace pycrypto dependency with pycryptodome.
|
|
||||||
- Removes Python 2 specific code.
|
|
||||||
|
|
||||||
|
|
||||||
1.0.0 (2019-10-31)
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- Removes broken method ``get_contacts()``.
|
|
||||||
- Adds support for login with a v2 Mega user account.
|
|
||||||
- Adds ``export()`` method to share a file or folder, returning public share URL with key.
|
|
||||||
- Adds code, message attrs to RequestError exception, makes message in raised exceptions include more details.
|
|
||||||
- Alters ``create_folder()`` to accept a path including multiple sub directories, adds support to create them all (similar to 'mkdir -p' on unix systems).
|
|
||||||
- Adds ``exclude_deleted=True`` optional arg to ``find()`` method, to exclude deleted nodes from results.
|
|
||||||
|
|
||||||
0.9.20 (2019-10-17)
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
- Python 3 bugfix to ``upload`` method.
|
|
||||||
|
|
||||||
0.9.19 (2019-10-16)
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
- Python 3 support and bugfixes.
|
|
||||||
- Update packaging code.
|
|
||||||
- Added changelog.
|
|
||||||
|
|
||||||
0.9.18 (2013-07-04)
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
- Unknown
|
|
|
@ -1,9 +1,9 @@
|
||||||
include HISTORY.rst
|
include HISTORY.md
|
||||||
include README.rst
|
include README.md
|
||||||
include requirements.txt
|
include requirements.txt
|
||||||
|
|
||||||
recursive-include tests *
|
recursive-include tests *
|
||||||
recursive-exclude * __pycache__
|
recursive-exclude * __pycache__
|
||||||
recursive-exclude * *.py[co]
|
recursive-exclude * *.py[co]
|
||||||
|
|
||||||
recursive-include docs *.rst conf.py Makefile make.bat
|
recursive-include docs *.md conf.py Makefile make.bat
|
||||||
|
|
169
README.md
Normal file
169
README.md
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
Mega.py
|
||||||
|
=======
|
||||||
|
|
||||||
|
[![Build
|
||||||
|
Status](https://travis-ci.org/odwyersoftware/mega.py.png?branch=master)](https://travis-ci.org/odwyersoftware/mega.py)
|
||||||
|
[![Downloads](https://pypip.in/d/mega.py/badge.png)](https://crate.io/packages/mega.py/) [![PyPI version](https://badge.fury.io/py/mega.py.svg)](https://pypi.org/project/mega.py/)
|
||||||
|
|
||||||
|
Python library for the [Mega.co.nz](https://mega.nz/aff=Zo6IxNaHw14)
|
||||||
|
API, currently supporting:
|
||||||
|
|
||||||
|
- login
|
||||||
|
- uploading
|
||||||
|
- downloading
|
||||||
|
- deleting
|
||||||
|
- searching
|
||||||
|
- sharing
|
||||||
|
- renaming
|
||||||
|
- moving files
|
||||||
|
|
||||||
|
This is a work in progress, further functionality coming shortly.
|
||||||
|
|
||||||
|
For more detailed information see API\_INFO.md
|
||||||
|
|
||||||
|
How To Use
|
||||||
|
----------
|
||||||
|
|
||||||
|
### Create a Mega account
|
||||||
|
|
||||||
|
First, [create an account with Mega](https://mega.nz/aff=Zo6IxNaHw14) .
|
||||||
|
|
||||||
|
### Install mega.py package
|
||||||
|
|
||||||
|
Run the following command, or run setup from the latest github source.
|
||||||
|
|
||||||
|
```python
|
||||||
|
pip install mega.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import mega.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mega import Mega
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create an instance of Mega.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
mega = Mega()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login to Mega
|
||||||
|
|
||||||
|
```python
|
||||||
|
m = mega.login(email, password)
|
||||||
|
# login using a temporary anonymous account
|
||||||
|
m = mega.login()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get user details
|
||||||
|
|
||||||
|
```python
|
||||||
|
details = m.get_user()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get account balance (Pro accounts only)
|
||||||
|
|
||||||
|
```python
|
||||||
|
balance = m.get_balance()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get account disk quota
|
||||||
|
|
||||||
|
```python
|
||||||
|
quota = m.get_quota()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get account storage space
|
||||||
|
|
||||||
|
```python
|
||||||
|
# specify unit output kilo, mega, gig, else bytes will output
|
||||||
|
space = m.get_storage_space(kilo=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get account files
|
||||||
|
|
||||||
|
```python
|
||||||
|
files = m.get_files()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload a file, and get its public link
|
||||||
|
|
||||||
|
```python
|
||||||
|
file = m.upload('myfile.doc')
|
||||||
|
m.get_upload_link(file)
|
||||||
|
# see mega.py for destination and filename options
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export a file or folder
|
||||||
|
|
||||||
|
```python
|
||||||
|
public_exported_web_link = m.export('myfile.doc')
|
||||||
|
public_exported_web_link = m.export('my_mega_folder/my_sub_folder_to_share')
|
||||||
|
# e.g. https://mega.nz/#F!WlVl1CbZ!M3wmhwZDENMNUJoBsdzFng
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find a file or folder
|
||||||
|
|
||||||
|
```python
|
||||||
|
folder = m.find('my_mega_folder')
|
||||||
|
# Excludes results which are in the Trash folder (i.e. deleted)
|
||||||
|
folder = m.find('my_mega_folder', exclude_deleted=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload a file to a destination folder
|
||||||
|
|
||||||
|
```python
|
||||||
|
folder = m.find('my_mega_folder')
|
||||||
|
m.upload('myfile.doc', folder[0])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download a file from URL or file obj, optionally specify destination folder
|
||||||
|
|
||||||
|
```python
|
||||||
|
file = m.find('myfile.doc')
|
||||||
|
m.download(file)
|
||||||
|
m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc')
|
||||||
|
m.download(file, '/home/john-smith/Desktop')
|
||||||
|
# specify optional download filename (download_url() supports this also)
|
||||||
|
m.download(file, '/home/john-smith/Desktop', 'myfile.zip')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import a file from URL, optionally specify destination folder
|
||||||
|
|
||||||
|
```python
|
||||||
|
m.import_public_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc')
|
||||||
|
folder_node = m.find('Documents')[1]
|
||||||
|
m.import_public_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc', dest_node=folder_node)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a folder
|
||||||
|
|
||||||
|
```python
|
||||||
|
m.create_folder('new_folder')
|
||||||
|
m.create_folder('new_folder/sub_folder/subsub_folder')
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns a dict of folder node name and node\_id, e.g.
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'new_folder': 'qpFhAYwA',
|
||||||
|
'sub_folder': '2pdlmY4Z',
|
||||||
|
'subsub_folder': 'GgMFCKLZ'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rename a file or a folder
|
||||||
|
|
||||||
|
```python
|
||||||
|
file = m.find('myfile.doc')
|
||||||
|
m.rename(file, 'my_file.doc')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contact Support
|
||||||
|
|
||||||
|
For paid priority support contact [mega@odwyer.software](mailto:mega@odwyer.software).
|
||||||
|
**[UK Python Development Agency](https://odwyer.software/)**
|
||||||
|
|
||||||
|
|
204
README.rst
204
README.rst
|
@ -1,204 +0,0 @@
|
||||||
**NOTICE**: If you're reading this on GitHub.com please be aware this is
|
|
||||||
a mirror of the primary remote located at https://code.richard.do/richardARPANET/mega.py_. Please direct issues and
|
|
||||||
pull requests there.
|
|
||||||
|
|
||||||
--------------
|
|
||||||
|
|
||||||
.. _megapy:
|
|
||||||
|
|
||||||
Mega.py
|
|
||||||
=======
|
|
||||||
|
|
||||||
|Build Status| |Downloads|
|
|
||||||
|
|
||||||
Python library for the `Mega.co.nz <https://mega.nz/aff=Zo6IxNaHw14>`_ API, currently supporting:
|
|
||||||
|
|
||||||
- login
|
|
||||||
- uploading
|
|
||||||
- downloading
|
|
||||||
- deleting
|
|
||||||
- searching
|
|
||||||
- sharing
|
|
||||||
- renaming
|
|
||||||
- moving files
|
|
||||||
|
|
||||||
This is a work in progress, further functionality coming shortly.
|
|
||||||
|
|
||||||
For more detailed information see API_INFO.md
|
|
||||||
|
|
||||||
How To Use
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. _create-mega-account:
|
|
||||||
|
|
||||||
Create a Mega account
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
First, `create an account with Mega <https://mega.nz/aff=Zo6IxNaHw14>`_
|
|
||||||
.
|
|
||||||
|
|
||||||
.. _install-megapy-package:
|
|
||||||
|
|
||||||
Install mega.py package
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
Run the following command, or run setup from the latest github source
|
|
||||||
pip install mega.py
|
|
||||||
|
|
||||||
.. _import-megapy:
|
|
||||||
|
|
||||||
Import mega.py
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
from mega import Mega
|
|
||||||
|
|
||||||
.. _create-an-instance-of-megapy:
|
|
||||||
|
|
||||||
Create an instance of Mega.py
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
mega = Mega()
|
|
||||||
|
|
||||||
Login to Mega
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
m = mega.login(email, password)
|
|
||||||
# login using a temporary anonymous account
|
|
||||||
m = mega.login()
|
|
||||||
|
|
||||||
Get user details
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
details = m.get_user()
|
|
||||||
|
|
||||||
Get account balance (Pro accounts only)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
balance = m.get_balance()
|
|
||||||
|
|
||||||
Get account disk quota
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
quota = m.get_quota()
|
|
||||||
|
|
||||||
Get account storage space
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
# specify unit output kilo, mega, gig, else bytes will output
|
|
||||||
space = m.get_storage_space(kilo=True)
|
|
||||||
|
|
||||||
Get account files
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
files = m.get_files()
|
|
||||||
|
|
||||||
Upload a file, and get its public link
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
file = m.upload('myfile.doc')
|
|
||||||
m.get_upload_link(file)
|
|
||||||
# see mega.py for destination and filename options
|
|
||||||
|
|
||||||
Export a file or folder
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
public_exported_web_link = m.export('myfile.doc')
|
|
||||||
public_exported_web_link = m.export('my_mega_folder/my_sub_folder_to_share')
|
|
||||||
# e.g. https://mega.nz/#F!WlVl1CbZ!M3wmhwZDENMNUJoBsdzFng
|
|
||||||
|
|
||||||
Find a file or folder
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
folder = m.find('my_mega_folder')
|
|
||||||
# Excludes results which are in the Trash folder (i.e. deleted)
|
|
||||||
folder = m.find('my_mega_folder', exclude_deleted=True)
|
|
||||||
|
|
||||||
Upload a file to a destination folder
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
folder = m.find('my_mega_folder')
|
|
||||||
m.upload('myfile.doc', folder[0])
|
|
||||||
|
|
||||||
Download a file from URL or file obj, optionally specify destination folder
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
file = m.find('myfile.doc')
|
|
||||||
m.download(file)
|
|
||||||
m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc')
|
|
||||||
m.download(file, '/home/john-smith/Desktop')
|
|
||||||
# specify optional download filename (download_url() supports this also)
|
|
||||||
m.download(file, '/home/john-smith/Desktop', 'myfile.zip')
|
|
||||||
|
|
||||||
Import a file from URL, optionally specify destination folder
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
m.import_public_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc')
|
|
||||||
folder_node = m.find('Documents')[1]
|
|
||||||
m.import_public_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc', dest_node=folder_node)
|
|
||||||
|
|
||||||
Create a folder
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
m.create_folder('new_folder')
|
|
||||||
m.create_folder('new_folder/sub_folder/subsub_folder')
|
|
||||||
|
|
||||||
Returns a dict of folder node name and node_id, e.g.
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
{
|
|
||||||
'new_folder': 'qpFhAYwA',
|
|
||||||
'sub_folder': '2pdlmY4Z',
|
|
||||||
'subsub_folder': 'GgMFCKLZ'
|
|
||||||
}
|
|
||||||
|
|
||||||
Rename a file or a folder
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
file = m.find('myfile.doc')
|
|
||||||
m.rename(file, 'my_file.doc')
|
|
||||||
|
|
||||||
|
|
||||||
~
|
|
||||||
|
|
||||||
.. _`https://code.richard.do/richardARPANET/mega.py`: https://code.richard.do/richardARPANET/mega.py
|
|
||||||
.. _`https://github.com/meganz/sdk`: https://github.com/meganz/sdk
|
|
||||||
|
|
||||||
.. |Build Status| image:: https://travis-ci.org/richardARPANET/mega.py.png?branch=master
|
|
||||||
:target: https://travis-ci.org/richardARPANET/mega.py
|
|
||||||
.. |Downloads| image:: https://pypip.in/d/mega.py/badge.png
|
|
||||||
:target: https://crate.io/packages/mega.py/
|
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
from mega import Mega
|
from mega import Mega
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +9,7 @@ def test():
|
||||||
comment/uncomment lines to test various parts of the API
|
comment/uncomment lines to test various parts of the API
|
||||||
see readme.md for more information
|
see readme.md for more information
|
||||||
"""
|
"""
|
||||||
|
unique = str(uuid.uuid4())
|
||||||
# user details
|
# user details
|
||||||
email = os.environ['EMAIL']
|
email = os.environ['EMAIL']
|
||||||
password = os.environ['PASS']
|
password = os.environ['PASS']
|
||||||
|
@ -36,10 +37,11 @@ def test():
|
||||||
print((files[file]))
|
print((files[file]))
|
||||||
|
|
||||||
# upload file
|
# upload file
|
||||||
print((m.upload('examples.py')))
|
print((m.upload(filename='examples.py',
|
||||||
|
dest_filename=f'examples_{unique}.py')))
|
||||||
|
|
||||||
# search for a file in account
|
# search for a file in account
|
||||||
file = m.find('examples.py')
|
file = m.find(f'examples_{unique}.py')
|
||||||
|
|
||||||
if file:
|
if file:
|
||||||
# get public link
|
# get public link
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
pytest
|
pytest==5.4.3
|
||||||
ipdb
|
ipdb==0.13.3
|
||||||
flake8
|
flake8==3.8.3
|
||||||
pep8-naming
|
pep8-naming==0.11.1
|
||||||
autoflake
|
autoflake==1.3.1
|
||||||
mccabe
|
mccabe==0.6.1
|
||||||
yapf
|
brunette==0.1.5
|
||||||
tox
|
tox==3.15.2
|
||||||
coverage
|
coverage==5.1
|
||||||
pytest-cov
|
pytest-cov==2.10.0
|
||||||
zest.releaser
|
zest.releaser==6.20.1
|
||||||
setuptools
|
setuptools==47.3.1
|
||||||
twine
|
twine==3.2.0
|
||||||
wheel
|
wheel==0.34.2
|
||||||
rope
|
rope==0.17.0
|
||||||
pytest-mock
|
pytest-mock==3.1.1
|
||||||
|
brunette==0.1.5
|
||||||
|
lock-requirements==0.1.1
|
||||||
|
requests-mock==1.8.0
|
||||||
|
|
16
setup.cfg
16
setup.cfg
|
@ -13,18 +13,10 @@ exclude = .git,__pycache__,legacy,build,dist,.tox
|
||||||
max-complexity = 15
|
max-complexity = 15
|
||||||
ignore = E741,W504,W503
|
ignore = E741,W504,W503
|
||||||
|
|
||||||
[yapf]
|
[tool:brunette]
|
||||||
based_on_style = pep8
|
line-length = 79
|
||||||
spaces_before_comment = 2
|
verbose = true
|
||||||
split_before_logical_operator = true
|
single-quotes = true
|
||||||
indent_width = 4
|
|
||||||
split_complex_comprehension = true
|
|
||||||
column_limit = 79
|
|
||||||
dedent_closing_brackets = true
|
|
||||||
spaces_around_power_operator = true
|
|
||||||
no_spaces_around_selected_binary_operators = false
|
|
||||||
split_penalty_import_names = 500
|
|
||||||
join_multiple_lines = true
|
|
||||||
|
|
||||||
[coverage:run]
|
[coverage:run]
|
||||||
omit =
|
omit =
|
||||||
|
|
49
setup.py
49
setup.py
|
@ -14,33 +14,30 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
||||||
with open('requirements.txt') as f:
|
with open('requirements.txt') as f:
|
||||||
install_requires = f.read().splitlines()
|
install_requires = f.read().splitlines()
|
||||||
|
|
||||||
with open('README.rst', 'r', encoding='utf-8') as rm_file:
|
with open('README.md', 'r', encoding='utf-8') as rm_file:
|
||||||
readme = rm_file.read()
|
readme = rm_file.read()
|
||||||
|
|
||||||
with open('HISTORY.rst', 'r', encoding='utf-8') as hist_file:
|
with open('HISTORY.md', 'r', encoding='utf-8') as hist_file:
|
||||||
history = hist_file.read()
|
history = hist_file.read()
|
||||||
|
|
||||||
setup(
|
setup(name='mega.py',
|
||||||
name='mega.py',
|
version='1.0.8.dev0',
|
||||||
version='1.0.7',
|
packages=find_packages('src', exclude=('tests', )),
|
||||||
packages=find_packages('src', exclude=('tests', )),
|
package_dir={'': 'src'},
|
||||||
package_dir={'': 'src'},
|
include_package_data=True,
|
||||||
include_package_data=True,
|
zip_safe=False,
|
||||||
zip_safe=False,
|
url='https://github.com/odwyersoftware/mega.py',
|
||||||
description='Python lib for the Mega.co.nz API',
|
description='Python lib for the Mega.co.nz API',
|
||||||
long_description=readme + '\n\n' + history,
|
long_description=readme + '\n\n' + history,
|
||||||
author='Richard O\'Dwyer',
|
long_description_content_type='text/markdown',
|
||||||
author_email='richard@richard.do',
|
author='O\'Dwyer Software',
|
||||||
license='Creative Commons Attribution-Noncommercial-Share Alike license',
|
author_email='hello@odwyer.software',
|
||||||
install_requires=install_requires,
|
license='Creative Commons Attribution-Noncommercial-Share Alike license',
|
||||||
classifiers=[
|
install_requires=install_requires,
|
||||||
'Intended Audience :: Developers',
|
classifiers=[
|
||||||
'Operating System :: OS Independent',
|
'Intended Audience :: Developers',
|
||||||
'Programming Language :: Python',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.7',
|
'Topic :: Internet :: WWW/HTTP',
|
||||||
'Programming Language :: Python :: 3.8',
|
])
|
||||||
'Topic :: Internet :: WWW/HTTP',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
|
@ -65,15 +65,13 @@ def prepare_key(arr):
|
||||||
|
|
||||||
|
|
||||||
def encrypt_key(a, key):
|
def encrypt_key(a, key):
|
||||||
return sum(
|
return sum((aes_cbc_encrypt_a32(a[i:i + 4], key)
|
||||||
(aes_cbc_encrypt_a32(a[i:i + 4], key) for i in range(0, len(a), 4)), ()
|
for i in range(0, len(a), 4)), ())
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_key(a, key):
|
def decrypt_key(a, key):
|
||||||
return sum(
|
return sum((aes_cbc_decrypt_a32(a[i:i + 4], key)
|
||||||
(aes_cbc_decrypt_a32(a[i:i + 4], key) for i in range(0, len(a), 4)), ()
|
for i in range(0, len(a), 4)), ())
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_attr(attr, key):
|
def encrypt_attr(attr, key):
|
||||||
|
|
|
@ -6,66 +6,41 @@ class ValidationError(Exception):
|
||||||
|
|
||||||
|
|
||||||
_CODE_TO_DESCRIPTIONS = {
|
_CODE_TO_DESCRIPTIONS = {
|
||||||
-1: (
|
-1: ('EINTERNAL',
|
||||||
'EINTERNAL',
|
('An internal error has occurred. Please submit a bug report, '
|
||||||
(
|
'detailing the exact circumstances in which this error occurred')),
|
||||||
'An internal error has occurred. Please submit a bug report, '
|
|
||||||
'detailing the exact circumstances in which this error occurred'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
-2: ('EARGS', 'You have passed invalid arguments to this command'),
|
-2: ('EARGS', 'You have passed invalid arguments to this command'),
|
||||||
-3: (
|
-3: ('EAGAIN',
|
||||||
'EAGAIN',
|
('(always at the request level) A temporary congestion or server '
|
||||||
(
|
'malfunction prevented your request from being processed. '
|
||||||
'(always at the request level) A temporary congestion or server '
|
'No data was altered. Retry. Retries must be spaced with '
|
||||||
'malfunction prevented your request from being processed. '
|
'exponential backoff')),
|
||||||
'No data was altered. Retry. Retries must be spaced with '
|
-4: ('ERATELIMIT',
|
||||||
'exponential backoff'
|
('You have exceeded your command weight per time quota. Please '
|
||||||
)
|
'wait a few seconds, then try again (this should never happen '
|
||||||
),
|
'in sane real-life applications)')),
|
||||||
-4: (
|
|
||||||
'ERATELIMIT',
|
|
||||||
(
|
|
||||||
'You have exceeded your command weight per time quota. Please '
|
|
||||||
'wait a few seconds, then try again (this should never happen '
|
|
||||||
'in sane real-life applications)'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
-5: ('EFAILED', 'The upload failed. Please restart it from scratch'),
|
-5: ('EFAILED', 'The upload failed. Please restart it from scratch'),
|
||||||
-6: (
|
-6:
|
||||||
'ETOOMANY',
|
('ETOOMANY',
|
||||||
'Too many concurrent IP addresses are accessing this upload target URL'
|
'Too many concurrent IP addresses are accessing this upload target URL'),
|
||||||
),
|
-7:
|
||||||
-7: (
|
('ERANGE', ('The upload file packet is out of range or not starting and '
|
||||||
'ERANGE',
|
'ending on a chunk boundary')),
|
||||||
(
|
-8: ('EEXPIRED',
|
||||||
'The upload file packet is out of range or not starting and '
|
('The upload target URL you are trying to access has expired. '
|
||||||
'ending on a chunk boundary'
|
'Please request a fresh one')),
|
||||||
)
|
|
||||||
),
|
|
||||||
-8: (
|
|
||||||
'EEXPIRED',
|
|
||||||
(
|
|
||||||
'The upload target URL you are trying to access has expired. '
|
|
||||||
'Please request a fresh one'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
-9: ('ENOENT', 'Object (typically, node or user) not found'),
|
-9: ('ENOENT', 'Object (typically, node or user) not found'),
|
||||||
-10: ('ECIRCULAR', 'Circular linkage attempted'),
|
-10: ('ECIRCULAR', 'Circular linkage attempted'),
|
||||||
-11: (
|
-11: ('EACCESS',
|
||||||
'EACCESS',
|
'Access violation (e.g., trying to write to a read-only share)'),
|
||||||
'Access violation (e.g., trying to write to a read-only share)'
|
|
||||||
),
|
|
||||||
-12: ('EEXIST', 'Trying to create an object that already exists'),
|
-12: ('EEXIST', 'Trying to create an object that already exists'),
|
||||||
-13: ('EINCOMPLETE', 'Trying to access an incomplete resource'),
|
-13: ('EINCOMPLETE', 'Trying to access an incomplete resource'),
|
||||||
-14: ('EKEY', 'A decryption operation failed (never returned by the API)'),
|
-14: ('EKEY', 'A decryption operation failed (never returned by the API)'),
|
||||||
-15: ('ESID', 'Invalid or expired user session, please relogin'),
|
-15: ('ESID', 'Invalid or expired user session, please relogin'),
|
||||||
-16: ('EBLOCKED', 'User blocked'),
|
-16: ('EBLOCKED', 'User blocked'),
|
||||||
-17: ('EOVERQUOTA', 'Request over quota'),
|
-17: ('EOVERQUOTA', 'Request over quota'),
|
||||||
-18: (
|
-18: ('ETEMPUNAVAIL',
|
||||||
'ETEMPUNAVAIL',
|
'Resource temporarily not available, please try again later'),
|
||||||
'Resource temporarily not available, please try again later'
|
|
||||||
),
|
|
||||||
-19: ('ETOOMANYCONNECTIONS', 'many connections on this resource'),
|
-19: ('ETOOMANYCONNECTIONS', 'many connections on this resource'),
|
||||||
-20: ('EWRITE', 'Write failed'),
|
-20: ('EWRITE', 'Write failed'),
|
||||||
-21: ('EREAD', 'Read failed'),
|
-21: ('EREAD', 'Read failed'),
|
||||||
|
|
560
src/mega/mega.py
560
src/mega/mega.py
|
@ -18,12 +18,11 @@ import requests
|
||||||
from tenacity import retry, wait_exponential, retry_if_exception_type
|
from tenacity import retry, wait_exponential, retry_if_exception_type
|
||||||
|
|
||||||
from .errors import ValidationError, RequestError
|
from .errors import ValidationError, RequestError
|
||||||
from .crypto import (
|
from .crypto import (a32_to_base64, encrypt_key, base64_url_encode,
|
||||||
a32_to_base64, encrypt_key, base64_url_encode, encrypt_attr, base64_to_a32,
|
encrypt_attr, base64_to_a32, base64_url_decode,
|
||||||
base64_url_decode, decrypt_attr, a32_to_str, get_chunks, str_to_a32,
|
decrypt_attr, a32_to_str, get_chunks, str_to_a32,
|
||||||
decrypt_key, mpi_to_int, stringhash, prepare_key, make_id, makebyte,
|
decrypt_key, mpi_to_int, stringhash, prepare_key, make_id,
|
||||||
modular_inverse
|
makebyte, modular_inverse)
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -64,13 +63,11 @@ class Mega:
|
||||||
user_hash = stringhash(email, password_aes)
|
user_hash = stringhash(email, password_aes)
|
||||||
else:
|
else:
|
||||||
# v2 user account
|
# v2 user account
|
||||||
pbkdf2_key = hashlib.pbkdf2_hmac(
|
pbkdf2_key = hashlib.pbkdf2_hmac(hash_name='sha512',
|
||||||
hash_name='sha512',
|
password=password.encode(),
|
||||||
password=password.encode(),
|
salt=a32_to_str(user_salt),
|
||||||
salt=a32_to_str(user_salt),
|
iterations=100000,
|
||||||
iterations=100000,
|
dklen=32)
|
||||||
dklen=32
|
|
||||||
)
|
|
||||||
password_aes = str_to_a32(pbkdf2_key[:16])
|
password_aes = str_to_a32(pbkdf2_key[:16])
|
||||||
user_hash = base64_url_encode(pbkdf2_key[-16:])
|
user_hash = base64_url_encode(pbkdf2_key[-16:])
|
||||||
resp = self._api_request({'a': 'us', 'user': email, 'uh': user_hash})
|
resp = self._api_request({'a': 'us', 'user': email, 'uh': user_hash})
|
||||||
|
@ -84,20 +81,16 @@ class Mega:
|
||||||
password_key = [random.randint(0, 0xFFFFFFFF)] * 4
|
password_key = [random.randint(0, 0xFFFFFFFF)] * 4
|
||||||
session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4
|
session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4
|
||||||
|
|
||||||
user = self._api_request(
|
user = self._api_request({
|
||||||
{
|
'a':
|
||||||
'a':
|
'up',
|
||||||
'up',
|
'k':
|
||||||
'k':
|
a32_to_base64(encrypt_key(master_key, password_key)),
|
||||||
a32_to_base64(encrypt_key(master_key, password_key)),
|
'ts':
|
||||||
'ts':
|
base64_url_encode(
|
||||||
base64_url_encode(
|
a32_to_str(session_self_challenge) +
|
||||||
a32_to_str(session_self_challenge) + a32_to_str(
|
a32_to_str(encrypt_key(session_self_challenge, master_key)))
|
||||||
encrypt_key(session_self_challenge, master_key)
|
})
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
resp = self._api_request({'a': 'us', 'user': user})
|
resp = self._api_request({'a': 'us', 'user': user})
|
||||||
if isinstance(resp, int):
|
if isinstance(resp, int):
|
||||||
|
@ -110,15 +103,13 @@ class Mega:
|
||||||
if 'tsid' in resp:
|
if 'tsid' in resp:
|
||||||
tsid = base64_url_decode(resp['tsid'])
|
tsid = base64_url_decode(resp['tsid'])
|
||||||
key_encrypted = a32_to_str(
|
key_encrypted = a32_to_str(
|
||||||
encrypt_key(str_to_a32(tsid[:16]), self.master_key)
|
encrypt_key(str_to_a32(tsid[:16]), self.master_key))
|
||||||
)
|
|
||||||
if key_encrypted == tsid[-16:]:
|
if key_encrypted == tsid[-16:]:
|
||||||
self.sid = resp['tsid']
|
self.sid = resp['tsid']
|
||||||
elif 'csid' in resp:
|
elif 'csid' in resp:
|
||||||
encrypted_rsa_private_key = base64_to_a32(resp['privk'])
|
encrypted_rsa_private_key = base64_to_a32(resp['privk'])
|
||||||
rsa_private_key = decrypt_key(
|
rsa_private_key = decrypt_key(encrypted_rsa_private_key,
|
||||||
encrypted_rsa_private_key, self.master_key
|
self.master_key)
|
||||||
)
|
|
||||||
|
|
||||||
private_key = a32_to_str(rsa_private_key)
|
private_key = a32_to_str(rsa_private_key)
|
||||||
# The private_key contains 4 MPI integers concatenated together.
|
# The private_key contains 4 MPI integers concatenated together.
|
||||||
|
@ -158,10 +149,8 @@ class Mega:
|
||||||
sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid)
|
sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid)
|
||||||
self.sid = base64_url_encode(sid[:43])
|
self.sid = base64_url_encode(sid[:43])
|
||||||
|
|
||||||
@retry(
|
@retry(retry=retry_if_exception_type(RuntimeError),
|
||||||
retry=retry_if_exception_type(RuntimeError),
|
wait=wait_exponential(multiplier=2, min=2, max=60))
|
||||||
wait=wait_exponential(multiplier=2, min=2, max=60)
|
|
||||||
)
|
|
||||||
def _api_request(self, data):
|
def _api_request(self, data):
|
||||||
params = {'id': self.sequence_num}
|
params = {'id': self.sequence_num}
|
||||||
self.sequence_num += 1
|
self.sequence_num += 1
|
||||||
|
@ -174,24 +163,42 @@ class Mega:
|
||||||
data = [data]
|
data = [data]
|
||||||
|
|
||||||
url = f'{self.schema}://g.api.{self.domain}/cs'
|
url = f'{self.schema}://g.api.{self.domain}/cs'
|
||||||
req = requests.post(
|
response = requests.post(
|
||||||
url,
|
url,
|
||||||
params=params,
|
params=params,
|
||||||
data=json.dumps(data),
|
data=json.dumps(data),
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
)
|
)
|
||||||
json_resp = json.loads(req.text)
|
json_resp = json.loads(response.text)
|
||||||
if isinstance(json_resp, int):
|
try:
|
||||||
if json_resp == -3:
|
if isinstance(json_resp, list):
|
||||||
|
int_resp = json_resp[0] if isinstance(json_resp[0],
|
||||||
|
int) else None
|
||||||
|
elif isinstance(json_resp, int):
|
||||||
|
int_resp = json_resp
|
||||||
|
except IndexError:
|
||||||
|
int_resp = None
|
||||||
|
if int_resp is not None:
|
||||||
|
if int_resp == 0:
|
||||||
|
return int_resp
|
||||||
|
if int_resp == -3:
|
||||||
msg = 'Request failed, retrying'
|
msg = 'Request failed, retrying'
|
||||||
logger.info(msg)
|
logger.info(msg)
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
raise RequestError(json_resp)
|
raise RequestError(int_resp)
|
||||||
return json_resp[0]
|
return json_resp[0]
|
||||||
|
|
||||||
def _parse_url(self, url):
|
def _parse_url(self, url):
|
||||||
# parse file id and key from url
|
"""Parse file id and key from url."""
|
||||||
if '!' in url:
|
if '/file/' in url:
|
||||||
|
# V2 URL structure
|
||||||
|
url = url.replace(' ', '')
|
||||||
|
file_id = re.findall(r'\W\w\w\w\w\w\w\w\w\W', url)[0][1:-1]
|
||||||
|
id_index = re.search(file_id, url).end()
|
||||||
|
key = url[id_index + 1:]
|
||||||
|
return f'{file_id}!{key}'
|
||||||
|
elif '!' in url:
|
||||||
|
# V1 URL structure
|
||||||
match = re.findall(r'/#!(.*)', url)
|
match = re.findall(r'/#!(.*)', url)
|
||||||
path = match[0]
|
path = match[0]
|
||||||
return path
|
return path
|
||||||
|
@ -201,10 +208,8 @@ class Mega:
|
||||||
def _process_file(self, file, shared_keys):
|
def _process_file(self, file, shared_keys):
|
||||||
if file['t'] == 0 or file['t'] == 1:
|
if file['t'] == 0 or file['t'] == 1:
|
||||||
keys = dict(
|
keys = dict(
|
||||||
keypart.split(':', 1)
|
keypart.split(':', 1) for keypart in file['k'].split('/')
|
||||||
for keypart in file['k'].split('/')
|
if ':' in keypart)
|
||||||
if ':' in keypart
|
|
||||||
)
|
|
||||||
uid = file['u']
|
uid = file['u']
|
||||||
key = None
|
key = None
|
||||||
# my objects
|
# my objects
|
||||||
|
@ -212,9 +217,8 @@ class Mega:
|
||||||
key = decrypt_key(base64_to_a32(keys[uid]), self.master_key)
|
key = decrypt_key(base64_to_a32(keys[uid]), self.master_key)
|
||||||
# shared folders
|
# shared folders
|
||||||
elif 'su' in file and 'sk' in file and ':' in file['k']:
|
elif 'su' in file and 'sk' in file and ':' in file['k']:
|
||||||
shared_key = decrypt_key(
|
shared_key = decrypt_key(base64_to_a32(file['sk']),
|
||||||
base64_to_a32(file['sk']), self.master_key
|
self.master_key)
|
||||||
)
|
|
||||||
key = decrypt_key(base64_to_a32(keys[file['h']]), shared_key)
|
key = decrypt_key(base64_to_a32(keys[file['h']]), shared_key)
|
||||||
if file['su'] not in shared_keys:
|
if file['su'] not in shared_keys:
|
||||||
shared_keys[file['su']] = {}
|
shared_keys[file['su']] = {}
|
||||||
|
@ -230,17 +234,14 @@ class Mega:
|
||||||
if file['h'] and file['h'] in shared_keys.get('EXP', ()):
|
if file['h'] and file['h'] in shared_keys.get('EXP', ()):
|
||||||
shared_key = shared_keys['EXP'][file['h']]
|
shared_key = shared_keys['EXP'][file['h']]
|
||||||
encrypted_key = str_to_a32(
|
encrypted_key = str_to_a32(
|
||||||
base64_url_decode(file['k'].split(':')[-1])
|
base64_url_decode(file['k'].split(':')[-1]))
|
||||||
)
|
|
||||||
key = decrypt_key(encrypted_key, shared_key)
|
key = decrypt_key(encrypted_key, shared_key)
|
||||||
file['shared_folder_key'] = shared_key
|
file['shared_folder_key'] = shared_key
|
||||||
if key is not None:
|
if key is not None:
|
||||||
# file
|
# file
|
||||||
if file['t'] == 0:
|
if file['t'] == 0:
|
||||||
k = (
|
k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6],
|
||||||
key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6],
|
key[3] ^ key[7])
|
||||||
key[3] ^ key[7]
|
|
||||||
)
|
|
||||||
file['iv'] = key[4:6] + (0, 0)
|
file['iv'] = key[4:6] + (0, 0)
|
||||||
file['meta_mac'] = key[6:8]
|
file['meta_mac'] = key[6:8]
|
||||||
# folder
|
# folder
|
||||||
|
@ -275,9 +276,8 @@ class Mega:
|
||||||
"""
|
"""
|
||||||
ok_dict = {}
|
ok_dict = {}
|
||||||
for ok_item in files['ok']:
|
for ok_item in files['ok']:
|
||||||
shared_key = decrypt_key(
|
shared_key = decrypt_key(base64_to_a32(ok_item['k']),
|
||||||
base64_to_a32(ok_item['k']), self.master_key
|
self.master_key)
|
||||||
)
|
|
||||||
ok_dict[ok_item['h']] = shared_key
|
ok_dict[ok_item['h']] = shared_key
|
||||||
for s_item in files['s']:
|
for s_item in files['s']:
|
||||||
if s_item['u'] not in shared_keys:
|
if s_item['u'] not in shared_keys:
|
||||||
|
@ -302,10 +302,8 @@ class Mega:
|
||||||
for foldername in paths:
|
for foldername in paths:
|
||||||
if foldername != '':
|
if foldername != '':
|
||||||
for file in files.items():
|
for file in files.items():
|
||||||
if (
|
if (file[1]['a'] and file[1]['t']
|
||||||
file[1]['a'] and file[1]['t']
|
and file[1]['a']['n'] == foldername):
|
||||||
and file[1]['a']['n'] == foldername
|
|
||||||
):
|
|
||||||
if parent_desc == file[1]['p']:
|
if parent_desc == file[1]['p']:
|
||||||
parent_desc = file[0]
|
parent_desc = file[0]
|
||||||
found = True
|
found = True
|
||||||
|
@ -327,28 +325,25 @@ class Mega:
|
||||||
parent_dir_name = path.parent.name
|
parent_dir_name = path.parent.name
|
||||||
for file in list(files.items()):
|
for file in list(files.items()):
|
||||||
parent_node_id = None
|
parent_node_id = None
|
||||||
if parent_dir_name:
|
try:
|
||||||
parent_node_id = self.find_path_descriptor(
|
if parent_dir_name:
|
||||||
parent_dir_name, files=files
|
parent_node_id = self.find_path_descriptor(parent_dir_name,
|
||||||
)
|
files=files)
|
||||||
if (
|
if (filename and parent_node_id and file[1]['a']
|
||||||
filename and parent_node_id and file[1]['a']
|
and file[1]['a']['n'] == filename
|
||||||
and file[1]['a']['n'] == filename
|
and parent_node_id == file[1]['p']):
|
||||||
and parent_node_id == file[1]['p']
|
if (exclude_deleted and self._trash_folder_node_id
|
||||||
):
|
== file[1]['p']):
|
||||||
if (
|
continue
|
||||||
exclude_deleted
|
return file
|
||||||
and self._trash_folder_node_id == file[1]['p']
|
elif (filename and file[1]['a']
|
||||||
):
|
and file[1]['a']['n'] == filename):
|
||||||
|
if (exclude_deleted
|
||||||
|
and self._trash_folder_node_id == file[1]['p']):
|
||||||
continue
|
continue
|
||||||
return file
|
return file
|
||||||
if (filename and file[1]['a'] and file[1]['a']['n'] == filename):
|
except TypeError:
|
||||||
if (
|
continue
|
||||||
exclude_deleted
|
|
||||||
and self._trash_folder_node_id == file[1]['p']
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
return file
|
|
||||||
|
|
||||||
def get_files(self):
|
def get_files(self):
|
||||||
logger.info('Getting all files...')
|
logger.info('Getting all files...')
|
||||||
|
@ -373,17 +368,12 @@ class Mega:
|
||||||
public_handle = self._api_request({'a': 'l', 'n': file['h']})
|
public_handle = self._api_request({'a': 'l', 'n': file['h']})
|
||||||
file_key = file['k'][file['k'].index(':') + 1:]
|
file_key = file['k'][file['k'].index(':') + 1:]
|
||||||
decrypted_key = a32_to_base64(
|
decrypted_key = a32_to_base64(
|
||||||
decrypt_key(base64_to_a32(file_key), self.master_key)
|
decrypt_key(base64_to_a32(file_key), self.master_key))
|
||||||
)
|
return (f'{self.schema}://{self.domain}'
|
||||||
return (
|
f'/#!{public_handle}!{decrypted_key}')
|
||||||
f'{self.schema}://{self.domain}'
|
|
||||||
f'/#!{public_handle}!{decrypted_key}'
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError('''Upload() response required as input,
|
||||||
'''Upload() response required as input,
|
use get_link() for regular file input''')
|
||||||
use get_link() for regular file input'''
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_link(self, file):
|
def get_link(self, file):
|
||||||
"""
|
"""
|
||||||
|
@ -393,15 +383,11 @@ class Mega:
|
||||||
if 'h' in file and 'k' in file:
|
if 'h' in file and 'k' in file:
|
||||||
public_handle = self._api_request({'a': 'l', 'n': file['h']})
|
public_handle = self._api_request({'a': 'l', 'n': file['h']})
|
||||||
if public_handle == -11:
|
if public_handle == -11:
|
||||||
raise RequestError(
|
raise RequestError("Can't get a public link from that file "
|
||||||
"Can't get a public link from that file "
|
"(is this a shared file?)")
|
||||||
"(is this a shared file?)"
|
|
||||||
)
|
|
||||||
decrypted_key = a32_to_base64(file['key'])
|
decrypted_key = a32_to_base64(file['key'])
|
||||||
return (
|
return (f'{self.schema}://{self.domain}'
|
||||||
f'{self.schema}://{self.domain}'
|
f'/#!{public_handle}!{decrypted_key}')
|
||||||
f'/#!{public_handle}!{decrypted_key}'
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise ValidationError('File id and key must be present')
|
raise ValidationError('File id and key must be present')
|
||||||
|
|
||||||
|
@ -419,15 +405,11 @@ class Mega:
|
||||||
if 'h' in file and 'k' in file:
|
if 'h' in file and 'k' in file:
|
||||||
public_handle = self._api_request({'a': 'l', 'n': file['h']})
|
public_handle = self._api_request({'a': 'l', 'n': file['h']})
|
||||||
if public_handle == -11:
|
if public_handle == -11:
|
||||||
raise RequestError(
|
raise RequestError("Can't get a public link from that file "
|
||||||
"Can't get a public link from that file "
|
"(is this a shared file?)")
|
||||||
"(is this a shared file?)"
|
|
||||||
)
|
|
||||||
decrypted_key = a32_to_base64(file['shared_folder_key'])
|
decrypted_key = a32_to_base64(file['shared_folder_key'])
|
||||||
return (
|
return (f'{self.schema}://{self.domain}'
|
||||||
f'{self.schema}://{self.domain}'
|
f'/#F!{public_handle}!{decrypted_key}')
|
||||||
f'/#F!{public_handle}!{decrypted_key}'
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise ValidationError('File id and key must be present')
|
raise ValidationError('File id and key must be present')
|
||||||
|
|
||||||
|
@ -490,14 +472,12 @@ class Mega:
|
||||||
"""
|
"""
|
||||||
Get current remaining disk quota in MegaBytes
|
Get current remaining disk quota in MegaBytes
|
||||||
"""
|
"""
|
||||||
json_resp = self._api_request(
|
json_resp = self._api_request({
|
||||||
{
|
'a': 'uq',
|
||||||
'a': 'uq',
|
'xfer': 1,
|
||||||
'xfer': 1,
|
'strg': 1,
|
||||||
'strg': 1,
|
'v': 1
|
||||||
'v': 1
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
# convert bytes to megabyes
|
# convert bytes to megabyes
|
||||||
return json_resp['mstrg'] / 1048576
|
return json_resp['mstrg'] / 1048576
|
||||||
|
|
||||||
|
@ -551,13 +531,11 @@ class Mega:
|
||||||
"""
|
"""
|
||||||
Destroy a file by its private id
|
Destroy a file by its private id
|
||||||
"""
|
"""
|
||||||
return self._api_request(
|
return self._api_request({
|
||||||
{
|
'a': 'd',
|
||||||
'a': 'd',
|
'n': file_id,
|
||||||
'n': file_id,
|
'i': self.request_id
|
||||||
'i': self.request_id
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy_url(self, url):
|
def destroy_url(self, url):
|
||||||
"""
|
"""
|
||||||
|
@ -583,24 +561,20 @@ class Mega:
|
||||||
"""
|
"""
|
||||||
Download a file by it's file object
|
Download a file by it's file object
|
||||||
"""
|
"""
|
||||||
return self._download_file(
|
return self._download_file(file_handle=None,
|
||||||
file_handle=None,
|
file_key=None,
|
||||||
file_key=None,
|
file=file[1],
|
||||||
file=file[1],
|
dest_path=dest_path,
|
||||||
dest_path=dest_path,
|
dest_filename=dest_filename,
|
||||||
dest_filename=dest_filename,
|
is_public=False)
|
||||||
is_public=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def _export_file(self, node):
|
def _export_file(self, node):
|
||||||
node_data = self._node_data(node)
|
node_data = self._node_data(node)
|
||||||
self._api_request(
|
self._api_request([{
|
||||||
[{
|
'a': 'l',
|
||||||
'a': 'l',
|
'n': node_data['h'],
|
||||||
'n': node_data['h'],
|
'i': self.request_id
|
||||||
'i': self.request_id
|
}])
|
||||||
}]
|
|
||||||
)
|
|
||||||
return self.get_link(node)
|
return self.get_link(node)
|
||||||
|
|
||||||
def export(self, path=None, node_id=None):
|
def export(self, path=None, node_id=None):
|
||||||
|
@ -623,10 +597,8 @@ class Mega:
|
||||||
|
|
||||||
master_key_cipher = AES.new(a32_to_str(self.master_key), AES.MODE_ECB)
|
master_key_cipher = AES.new(a32_to_str(self.master_key), AES.MODE_ECB)
|
||||||
ha = base64_url_encode(
|
ha = base64_url_encode(
|
||||||
master_key_cipher.encrypt(
|
master_key_cipher.encrypt(node_data['h'].encode("utf8") +
|
||||||
node_data['h'].encode("utf8") + node_data['h'].encode("utf8")
|
node_data['h'].encode("utf8")))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
share_key = secrets.token_bytes(16)
|
share_key = secrets.token_bytes(16)
|
||||||
ok = base64_url_encode(master_key_cipher.encrypt(share_key))
|
ok = base64_url_encode(master_key_cipher.encrypt(share_key))
|
||||||
|
@ -634,24 +606,26 @@ class Mega:
|
||||||
share_key_cipher = AES.new(share_key, AES.MODE_ECB)
|
share_key_cipher = AES.new(share_key, AES.MODE_ECB)
|
||||||
node_key = node_data['k']
|
node_key = node_data['k']
|
||||||
encrypted_node_key = base64_url_encode(
|
encrypted_node_key = base64_url_encode(
|
||||||
share_key_cipher.encrypt(a32_to_str(node_key))
|
share_key_cipher.encrypt(a32_to_str(node_key)))
|
||||||
)
|
|
||||||
|
|
||||||
node_id = node_data['h']
|
node_id = node_data['h']
|
||||||
request_body = [
|
request_body = [{
|
||||||
{
|
'a':
|
||||||
'a': 's2',
|
's2',
|
||||||
'n': node_id,
|
'n':
|
||||||
's': [{
|
node_id,
|
||||||
'u': 'EXP',
|
's': [{
|
||||||
'r': 0
|
'u': 'EXP',
|
||||||
}],
|
'r': 0
|
||||||
'i': self.request_id,
|
}],
|
||||||
'ok': ok,
|
'i':
|
||||||
'ha': ha,
|
self.request_id,
|
||||||
'cr': [[node_id], [node_id], [0, 0, encrypted_node_key]]
|
'ok':
|
||||||
}
|
ok,
|
||||||
]
|
'ha':
|
||||||
|
ha,
|
||||||
|
'cr': [[node_id], [node_id], [0, 0, encrypted_node_key]]
|
||||||
|
}]
|
||||||
self._api_request(request_body)
|
self._api_request(request_body)
|
||||||
nodes = self.get_files()
|
nodes = self.get_files()
|
||||||
return self.get_folder_link(nodes[node_id])
|
return self.get_folder_link(nodes[node_id])
|
||||||
|
@ -671,38 +645,30 @@ class Mega:
|
||||||
is_public=True,
|
is_public=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _download_file(
|
def _download_file(self,
|
||||||
self,
|
file_handle,
|
||||||
file_handle,
|
file_key,
|
||||||
file_key,
|
dest_path=None,
|
||||||
dest_path=None,
|
dest_filename=None,
|
||||||
dest_filename=None,
|
is_public=False,
|
||||||
is_public=False,
|
file=None):
|
||||||
file=None
|
|
||||||
):
|
|
||||||
if file is None:
|
if file is None:
|
||||||
if is_public:
|
if is_public:
|
||||||
file_key = base64_to_a32(file_key)
|
file_key = base64_to_a32(file_key)
|
||||||
file_data = self._api_request(
|
file_data = self._api_request({
|
||||||
{
|
'a': 'g',
|
||||||
'a': 'g',
|
'g': 1,
|
||||||
'g': 1,
|
'p': file_handle
|
||||||
'p': file_handle
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
file_data = self._api_request(
|
file_data = self._api_request({
|
||||||
{
|
'a': 'g',
|
||||||
'a': 'g',
|
'g': 1,
|
||||||
'g': 1,
|
'n': file_handle
|
||||||
'n': file_handle
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
k = (
|
k = (file_key[0] ^ file_key[4], file_key[1] ^ file_key[5],
|
||||||
file_key[0] ^ file_key[4], file_key[1] ^ file_key[5],
|
file_key[2] ^ file_key[6], file_key[3] ^ file_key[7])
|
||||||
file_key[2] ^ file_key[6], file_key[3] ^ file_key[7]
|
|
||||||
)
|
|
||||||
iv = file_key[4:6] + (0, 0)
|
iv = file_key[4:6] + (0, 0)
|
||||||
meta_mac = file_key[6:8]
|
meta_mac = file_key[6:8]
|
||||||
else:
|
else:
|
||||||
|
@ -733,19 +699,17 @@ class Mega:
|
||||||
else:
|
else:
|
||||||
dest_path += '/'
|
dest_path += '/'
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(mode='w+b',
|
||||||
mode='w+b', prefix='megapy_', delete=False
|
prefix='megapy_',
|
||||||
) as temp_output_file:
|
delete=False) as temp_output_file:
|
||||||
k_str = a32_to_str(k)
|
k_str = a32_to_str(k)
|
||||||
counter = Counter.new(
|
counter = Counter.new(128,
|
||||||
128, initial_value=((iv[0] << 32) + iv[1]) << 64
|
initial_value=((iv[0] << 32) + iv[1]) << 64)
|
||||||
)
|
|
||||||
aes = AES.new(k_str, AES.MODE_CTR, counter=counter)
|
aes = AES.new(k_str, AES.MODE_CTR, counter=counter)
|
||||||
|
|
||||||
mac_str = '\0' * 16
|
mac_str = '\0' * 16
|
||||||
mac_encryptor = AES.new(
|
mac_encryptor = AES.new(k_str, AES.MODE_CBC,
|
||||||
k_str, AES.MODE_CBC, mac_str.encode("utf8")
|
mac_str.encode("utf8"))
|
||||||
)
|
|
||||||
iv_str = a32_to_str([iv[0], iv[1], iv[0], iv[1]])
|
iv_str = a32_to_str([iv[0], iv[1], iv[0], iv[1]])
|
||||||
|
|
||||||
for chunk_start, chunk_size in get_chunks(file_size):
|
for chunk_start, chunk_size in get_chunks(file_size):
|
||||||
|
@ -770,14 +734,12 @@ class Mega:
|
||||||
mac_str = mac_encryptor.encrypt(encryptor.encrypt(block))
|
mac_str = mac_encryptor.encrypt(encryptor.encrypt(block))
|
||||||
|
|
||||||
file_info = os.stat(temp_output_file.name)
|
file_info = os.stat(temp_output_file.name)
|
||||||
logger.info(
|
logger.info('%s of %s downloaded', file_info.st_size,
|
||||||
'%s of %s downloaded', file_info.st_size, file_size
|
file_size)
|
||||||
)
|
|
||||||
file_mac = str_to_a32(mac_str)
|
file_mac = str_to_a32(mac_str)
|
||||||
# check mac integrity
|
# check mac integrity
|
||||||
if (
|
if (file_mac[0] ^ file_mac[1],
|
||||||
file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]
|
file_mac[2] ^ file_mac[3]) != meta_mac:
|
||||||
) != meta_mac:
|
|
||||||
raise ValueError('Mismatched mac')
|
raise ValueError('Mismatched mac')
|
||||||
output_path = Path(dest_path + file_name)
|
output_path = Path(dest_path + file_name)
|
||||||
shutil.move(temp_output_file.name, output_path)
|
shutil.move(temp_output_file.name, output_path)
|
||||||
|
@ -800,17 +762,15 @@ class Mega:
|
||||||
ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)]
|
ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)]
|
||||||
k_str = a32_to_str(ul_key[:4])
|
k_str = a32_to_str(ul_key[:4])
|
||||||
count = Counter.new(
|
count = Counter.new(
|
||||||
128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64
|
128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64)
|
||||||
)
|
|
||||||
aes = AES.new(k_str, AES.MODE_CTR, counter=count)
|
aes = AES.new(k_str, AES.MODE_CTR, counter=count)
|
||||||
|
|
||||||
upload_progress = 0
|
upload_progress = 0
|
||||||
completion_file_handle = None
|
completion_file_handle = None
|
||||||
|
|
||||||
mac_str = '\0' * 16
|
mac_str = '\0' * 16
|
||||||
mac_encryptor = AES.new(
|
mac_encryptor = AES.new(k_str, AES.MODE_CBC,
|
||||||
k_str, AES.MODE_CBC, mac_str.encode("utf8")
|
mac_str.encode("utf8"))
|
||||||
)
|
|
||||||
iv_str = a32_to_str([ul_key[4], ul_key[5], ul_key[4], ul_key[5]])
|
iv_str = a32_to_str([ul_key[4], ul_key[5], ul_key[4], ul_key[5]])
|
||||||
if file_size > 0:
|
if file_size > 0:
|
||||||
for chunk_start, chunk_size in get_chunks(file_size):
|
for chunk_start, chunk_size in get_chunks(file_size):
|
||||||
|
@ -835,19 +795,17 @@ class Mega:
|
||||||
|
|
||||||
# encrypt file and upload
|
# encrypt file and upload
|
||||||
chunk = aes.encrypt(chunk)
|
chunk = aes.encrypt(chunk)
|
||||||
output_file = requests.post(
|
output_file = requests.post(ul_url + "/" +
|
||||||
ul_url + "/" + str(chunk_start),
|
str(chunk_start),
|
||||||
data=chunk,
|
data=chunk,
|
||||||
timeout=self.timeout
|
timeout=self.timeout)
|
||||||
)
|
|
||||||
completion_file_handle = output_file.text
|
completion_file_handle = output_file.text
|
||||||
logger.info(
|
logger.info('%s of %s uploaded', upload_progress,
|
||||||
'%s of %s uploaded', upload_progress, file_size
|
file_size)
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
output_file = requests.post(
|
output_file = requests.post(ul_url + "/0",
|
||||||
ul_url + "/0", data='', timeout=self.timeout
|
data='',
|
||||||
)
|
timeout=self.timeout)
|
||||||
completion_file_handle = output_file.text
|
completion_file_handle = output_file.text
|
||||||
|
|
||||||
logger.info('Chunks uploaded')
|
logger.info('Chunks uploaded')
|
||||||
|
@ -862,8 +820,7 @@ class Mega:
|
||||||
attribs = {'n': dest_filename}
|
attribs = {'n': dest_filename}
|
||||||
|
|
||||||
encrypt_attribs = base64_url_encode(
|
encrypt_attribs = base64_url_encode(
|
||||||
encrypt_attr(attribs, ul_key[:4])
|
encrypt_attr(attribs, ul_key[:4]))
|
||||||
)
|
|
||||||
key = [
|
key = [
|
||||||
ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5],
|
ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5],
|
||||||
ul_key[2] ^ meta_mac[0], ul_key[3] ^ meta_mac[1], ul_key[4],
|
ul_key[2] ^ meta_mac[0], ul_key[3] ^ meta_mac[1], ul_key[4],
|
||||||
|
@ -872,21 +829,20 @@ class Mega:
|
||||||
encrypted_key = a32_to_base64(encrypt_key(key, self.master_key))
|
encrypted_key = a32_to_base64(encrypt_key(key, self.master_key))
|
||||||
logger.info('Sending request to update attributes')
|
logger.info('Sending request to update attributes')
|
||||||
# update attributes
|
# update attributes
|
||||||
data = self._api_request(
|
data = self._api_request({
|
||||||
{
|
'a':
|
||||||
'a': 'p',
|
'p',
|
||||||
't': dest,
|
't':
|
||||||
'i': self.request_id,
|
dest,
|
||||||
'n': [
|
'i':
|
||||||
{
|
self.request_id,
|
||||||
'h': completion_file_handle,
|
'n': [{
|
||||||
't': 0,
|
'h': completion_file_handle,
|
||||||
'a': encrypt_attribs,
|
't': 0,
|
||||||
'k': encrypted_key
|
'a': encrypt_attribs,
|
||||||
}
|
'k': encrypted_key
|
||||||
]
|
}]
|
||||||
}
|
})
|
||||||
)
|
|
||||||
logger.info('Upload complete')
|
logger.info('Upload complete')
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -900,21 +856,20 @@ class Mega:
|
||||||
encrypted_key = a32_to_base64(encrypt_key(ul_key[:4], self.master_key))
|
encrypted_key = a32_to_base64(encrypt_key(ul_key[:4], self.master_key))
|
||||||
|
|
||||||
# update attributes
|
# update attributes
|
||||||
data = self._api_request(
|
data = self._api_request({
|
||||||
{
|
'a':
|
||||||
'a': 'p',
|
'p',
|
||||||
't': parent_node_id,
|
't':
|
||||||
'n': [
|
parent_node_id,
|
||||||
{
|
'n': [{
|
||||||
'h': 'xxxxxxxx',
|
'h': 'xxxxxxxx',
|
||||||
't': 1,
|
't': 1,
|
||||||
'a': encrypt_attribs,
|
'a': encrypt_attribs,
|
||||||
'k': encrypted_key
|
'k': encrypted_key
|
||||||
}
|
}],
|
||||||
],
|
'i':
|
||||||
'i': self.request_id
|
self.request_id
|
||||||
}
|
})
|
||||||
)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _root_node_id(self):
|
def _root_node_id(self):
|
||||||
|
@ -937,9 +892,8 @@ class Mega:
|
||||||
parent_node_id = dest
|
parent_node_id = dest
|
||||||
else:
|
else:
|
||||||
parent_node_id = folder_node_ids[idx - 1]
|
parent_node_id = folder_node_ids[idx - 1]
|
||||||
created_node = self._mkdir(
|
created_node = self._mkdir(name=directory_name,
|
||||||
name=directory_name, parent_node_id=parent_node_id
|
parent_node_id=parent_node_id)
|
||||||
)
|
|
||||||
node_id = created_node['f'][0]['h']
|
node_id = created_node['f'][0]['h']
|
||||||
folder_node_ids[idx] = node_id
|
folder_node_ids[idx] = node_id
|
||||||
return dict(zip(dirs, folder_node_ids.values()))
|
return dict(zip(dirs, folder_node_ids.values()))
|
||||||
|
@ -950,21 +904,16 @@ class Mega:
|
||||||
attribs = {'n': new_name}
|
attribs = {'n': new_name}
|
||||||
# encrypt attribs
|
# encrypt attribs
|
||||||
encrypt_attribs = base64_url_encode(encrypt_attr(attribs, file['k']))
|
encrypt_attribs = base64_url_encode(encrypt_attr(attribs, file['k']))
|
||||||
encrypted_key = a32_to_base64(
|
encrypted_key = a32_to_base64(encrypt_key(file['key'],
|
||||||
encrypt_key(file['key'], self.master_key)
|
self.master_key))
|
||||||
)
|
|
||||||
# update attributes
|
# update attributes
|
||||||
return self._api_request(
|
return self._api_request([{
|
||||||
[
|
'a': 'a',
|
||||||
{
|
'attr': encrypt_attribs,
|
||||||
'a': 'a',
|
'key': encrypted_key,
|
||||||
'attr': encrypt_attribs,
|
'n': file['h'],
|
||||||
'key': encrypted_key,
|
'i': self.request_id
|
||||||
'n': file['h'],
|
}])
|
||||||
'i': self.request_id
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def move(self, file_id, target):
|
def move(self, file_id, target):
|
||||||
"""
|
"""
|
||||||
|
@ -994,14 +943,12 @@ class Mega:
|
||||||
else:
|
else:
|
||||||
file = target[1]
|
file = target[1]
|
||||||
target_node_id = file['h']
|
target_node_id = file['h']
|
||||||
return self._api_request(
|
return self._api_request({
|
||||||
{
|
'a': 'm',
|
||||||
'a': 'm',
|
'n': file_id,
|
||||||
'n': file_id,
|
't': target_node_id,
|
||||||
't': target_node_id,
|
'i': self.request_id
|
||||||
'i': self.request_id
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_contact(self, email):
|
def add_contact(self, email):
|
||||||
"""
|
"""
|
||||||
|
@ -1029,14 +976,12 @@ class Mega:
|
||||||
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
|
||||||
ValidationError('add_contact requires a valid email address')
|
ValidationError('add_contact requires a valid email address')
|
||||||
else:
|
else:
|
||||||
return self._api_request(
|
return self._api_request({
|
||||||
{
|
'a': 'ur',
|
||||||
'a': 'ur',
|
'u': email,
|
||||||
'u': email,
|
'l': l,
|
||||||
'l': l,
|
'i': self.request_id
|
||||||
'i': self.request_id
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_public_url_info(self, url):
|
def get_public_url_info(self, url):
|
||||||
"""
|
"""
|
||||||
|
@ -1050,9 +995,10 @@ class Mega:
|
||||||
Import the public url into user account
|
Import the public url into user account
|
||||||
"""
|
"""
|
||||||
file_handle, file_key = self._parse_url(url).split('!')
|
file_handle, file_key = self._parse_url(url).split('!')
|
||||||
return self.import_public_file(
|
return self.import_public_file(file_handle,
|
||||||
file_handle, file_key, dest_node=dest_node, dest_name=dest_name
|
file_key,
|
||||||
)
|
dest_node=dest_node,
|
||||||
|
dest_name=dest_name)
|
||||||
|
|
||||||
def get_public_file_info(self, file_handle, file_key):
|
def get_public_file_info(self, file_handle, file_key):
|
||||||
"""
|
"""
|
||||||
|
@ -1066,9 +1012,8 @@ class Mega:
|
||||||
raise ValueError("Unexpected result", data)
|
raise ValueError("Unexpected result", data)
|
||||||
|
|
||||||
key = base64_to_a32(file_key)
|
key = base64_to_a32(file_key)
|
||||||
k = (
|
k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6],
|
||||||
key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]
|
key[3] ^ key[7])
|
||||||
)
|
|
||||||
|
|
||||||
size = data['s']
|
size = data['s']
|
||||||
unencrypted_attrs = decrypt_attr(base64_url_decode(data['at']), k)
|
unencrypted_attrs = decrypt_attr(base64_url_decode(data['at']), k)
|
||||||
|
@ -1077,9 +1022,11 @@ class Mega:
|
||||||
result = {'size': size, 'name': unencrypted_attrs['n']}
|
result = {'size': size, 'name': unencrypted_attrs['n']}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def import_public_file(
|
def import_public_file(self,
|
||||||
self, file_handle, file_key, dest_node=None, dest_name=None
|
file_handle,
|
||||||
):
|
file_key,
|
||||||
|
dest_node=None,
|
||||||
|
dest_name=None):
|
||||||
"""
|
"""
|
||||||
Import the public file into user account
|
Import the public file into user account
|
||||||
"""
|
"""
|
||||||
|
@ -1094,23 +1041,20 @@ class Mega:
|
||||||
dest_name = pl_info['name']
|
dest_name = pl_info['name']
|
||||||
|
|
||||||
key = base64_to_a32(file_key)
|
key = base64_to_a32(file_key)
|
||||||
k = (
|
k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6],
|
||||||
key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]
|
key[3] ^ key[7])
|
||||||
)
|
|
||||||
|
|
||||||
encrypted_key = a32_to_base64(encrypt_key(key, self.master_key))
|
encrypted_key = a32_to_base64(encrypt_key(key, self.master_key))
|
||||||
encrypted_name = base64_url_encode(encrypt_attr({'n': dest_name}, k))
|
encrypted_name = base64_url_encode(encrypt_attr({'n': dest_name}, k))
|
||||||
return self._api_request(
|
return self._api_request({
|
||||||
{
|
'a':
|
||||||
'a': 'p',
|
'p',
|
||||||
't': dest_node['h'],
|
't':
|
||||||
'n': [
|
dest_node['h'],
|
||||||
{
|
'n': [{
|
||||||
'ph': file_handle,
|
'ph': file_handle,
|
||||||
't': 0,
|
't': 0,
|
||||||
'a': encrypted_name,
|
'a': encrypted_name,
|
||||||
'k': encrypted_key
|
'k': encrypted_key
|
||||||
}
|
}]
|
||||||
]
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
|
@ -3,39 +3,17 @@ import pytest
|
||||||
from mega.crypto import get_chunks
|
from mega.crypto import get_chunks
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize('file_size, exp_result', [
|
||||||
'file_size, exp_result', [
|
(10, ((0, 10), )),
|
||||||
(
|
(1000, ((0, 1000), )),
|
||||||
10,
|
(1000000, ((0, 131072), (131072, 262144), (393216, 393216),
|
||||||
(
|
(786432, 213568))),
|
||||||
(0, 10),
|
(10000000, ((0, 131072), (131072, 262144), (393216, 393216),
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1000,
|
|
||||||
(
|
|
||||||
(0, 1000),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
1000000,
|
|
||||||
(
|
|
||||||
(0, 131072), (131072, 262144), (393216, 393216),
|
|
||||||
(786432, 213568)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
(
|
|
||||||
10000000,
|
|
||||||
(
|
|
||||||
(0, 131072), (131072, 262144), (393216, 393216),
|
|
||||||
(786432, 524288), (1310720, 655360), (1966080, 786432),
|
(786432, 524288), (1310720, 655360), (1966080, 786432),
|
||||||
(2752512, 917504), (3670016, 1048576), (4718592, 1048576),
|
(2752512, 917504), (3670016, 1048576), (4718592, 1048576),
|
||||||
(5767168, 1048576), (6815744, 1048576), (7864320, 1048576),
|
(5767168, 1048576), (6815744, 1048576), (7864320, 1048576),
|
||||||
(8912896, 1048576), (9961472, 38528)
|
(8912896, 1048576), (9961472, 38528))),
|
||||||
)
|
])
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
def test_get_chunks(file_size, exp_result):
|
def test_get_chunks(file_size, exp_result):
|
||||||
result = tuple(get_chunks(file_size))
|
result = tuple(get_chunks(file_size))
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,9 @@ import pytest
|
||||||
from mega.errors import RequestError, _CODE_TO_DESCRIPTIONS
|
from mega.errors import RequestError, _CODE_TO_DESCRIPTIONS
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize('code, exp_message',
|
||||||
'code, exp_message', [
|
[(code, f'{desc[0]}, {desc[1]}')
|
||||||
(code, f'{desc[0]}, {desc[1]}')
|
for code, desc in _CODE_TO_DESCRIPTIONS.items()])
|
||||||
for code, desc in _CODE_TO_DESCRIPTIONS.items()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
def test_request_error(code, exp_message):
|
def test_request_error(code, exp_message):
|
||||||
exc = RequestError(code)
|
exc = RequestError(code)
|
||||||
|
|
||||||
|
|
|
@ -2,15 +2,16 @@ import random
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import requests_mock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from mega import Mega
|
from mega import Mega
|
||||||
|
|
||||||
TEST_CONTACT = 'test@mega.co.nz'
|
TEST_CONTACT = 'test@mega.co.nz'
|
||||||
TEST_PUBLIC_URL = (
|
TEST_PUBLIC_URL = (
|
||||||
'https://mega.nz/#!hYVmXKqL!r0d0-WRnFwulR_shhuEDwrY1Vo103-am1MyUy8oV6Ps'
|
'https://mega.nz/#!hYVmXKqL!r0d0-WRnFwulR_shhuEDwrY1Vo103-am1MyUy8oV6Ps')
|
||||||
)
|
|
||||||
TEST_FILE = os.path.basename(__file__)
|
TEST_FILE = os.path.basename(__file__)
|
||||||
|
MODULE = 'mega.mega'
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -32,9 +33,7 @@ def mega(folder_name):
|
||||||
def uploaded_file(mega, folder_name):
|
def uploaded_file(mega, folder_name):
|
||||||
folder = mega.find(folder_name)
|
folder = mega.find(folder_name)
|
||||||
dest_node_id = folder[1]['h']
|
dest_node_id = folder[1]['h']
|
||||||
mega.upload(
|
mega.upload(__file__, dest=dest_node_id, dest_filename='test.py')
|
||||||
__file__, dest=dest_node_id, dest_filename='test.py'
|
|
||||||
)
|
|
||||||
path = f'{folder_name}/test.py'
|
path = f'{folder_name}/test.py'
|
||||||
return mega.find(path)
|
return mega.find(path)
|
||||||
|
|
||||||
|
@ -72,8 +71,8 @@ def test_get_link(mega, uploaded_file):
|
||||||
assert isinstance(link, str)
|
assert isinstance(link, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
class TestExport:
|
class TestExport:
|
||||||
|
|
||||||
def test_export_folder(self, mega, folder_name):
|
def test_export_folder(self, mega, folder_name):
|
||||||
public_url = None
|
public_url = None
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
|
@ -103,9 +102,7 @@ class TestExport:
|
||||||
# Upload a single file into a folder
|
# Upload a single file into a folder
|
||||||
folder = mega.find(folder_name)
|
folder = mega.find(folder_name)
|
||||||
dest_node_id = folder[1]['h']
|
dest_node_id = folder[1]['h']
|
||||||
mega.upload(
|
mega.upload(__file__, dest=dest_node_id, dest_filename='test.py')
|
||||||
__file__, dest=dest_node_id, dest_filename='test.py'
|
|
||||||
)
|
|
||||||
path = f'{folder_name}/test.py'
|
path = f'{folder_name}/test.py'
|
||||||
assert mega.find(path)
|
assert mega.find(path)
|
||||||
|
|
||||||
|
@ -131,8 +128,7 @@ class TestCreateFolder:
|
||||||
|
|
||||||
def test_create_folder_with_sub_folders(self, mega, folder_name, mocker):
|
def test_create_folder_with_sub_folders(self, mega, folder_name, mocker):
|
||||||
folder_names_and_node_ids = mega.create_folder(
|
folder_names_and_node_ids = mega.create_folder(
|
||||||
name=(Path(folder_name) / 'subdir' / 'anothersubdir')
|
name=(Path(folder_name) / 'subdir' / 'anothersubdir'))
|
||||||
)
|
|
||||||
|
|
||||||
assert len(folder_names_and_node_ids) == 3
|
assert len(folder_names_and_node_ids) == 3
|
||||||
assert folder_names_and_node_ids == {
|
assert folder_names_and_node_ids == {
|
||||||
|
@ -143,16 +139,21 @@ class TestCreateFolder:
|
||||||
|
|
||||||
|
|
||||||
class TestFind:
|
class TestFind:
|
||||||
|
|
||||||
def test_find_file(self, mega, folder_name):
|
def test_find_file(self, mega, folder_name):
|
||||||
folder = mega.find(folder_name)
|
folder = mega.find(folder_name)
|
||||||
|
assert folder
|
||||||
dest_node_id = folder[1]['h']
|
dest_node_id = folder[1]['h']
|
||||||
mega.upload(
|
mega.upload(__file__, dest=dest_node_id, dest_filename='test.py')
|
||||||
__file__, dest=dest_node_id, dest_filename='test.py'
|
file1 = mega.find(f'{folder_name}/test.py')
|
||||||
)
|
assert file1
|
||||||
path = f'{folder_name}/test.py'
|
|
||||||
|
|
||||||
assert mega.find(path)
|
dest_node_id2 = mega.create_folder('new_folder')['new_folder']
|
||||||
|
mega.upload(__file__, dest=dest_node_id2, dest_filename='test.py')
|
||||||
|
|
||||||
|
file2 = mega.find('new_folder/test.py')
|
||||||
|
assert file2
|
||||||
|
# Check that the correct test.py was found
|
||||||
|
assert file1 != file2
|
||||||
|
|
||||||
def test_path_not_found_returns_none(self, mega):
|
def test_path_not_found_returns_none(self, mega):
|
||||||
assert mega.find('not_found') is None
|
assert mega.find('not_found') is None
|
||||||
|
@ -194,15 +195,13 @@ def test_download(mega, tmpdir, folder_name):
|
||||||
# Upload a single file into a folder
|
# Upload a single file into a folder
|
||||||
folder = mega.find(folder_name)
|
folder = mega.find(folder_name)
|
||||||
dest_node_id = folder[1]['h']
|
dest_node_id = folder[1]['h']
|
||||||
mega.upload(
|
mega.upload(__file__, dest=dest_node_id, dest_filename='test.py')
|
||||||
__file__, dest=dest_node_id, dest_filename='test.py'
|
|
||||||
)
|
|
||||||
path = f'{folder_name}/test.py'
|
path = f'{folder_name}/test.py'
|
||||||
file = mega.find(path)
|
file = mega.find(path)
|
||||||
|
|
||||||
output_path = mega.download(
|
output_path = mega.download(file=file,
|
||||||
file=file, dest_path=tmpdir, dest_filename='test.py'
|
dest_path=tmpdir,
|
||||||
)
|
dest_filename='test.py')
|
||||||
|
|
||||||
assert output_path.exists()
|
assert output_path.exists()
|
||||||
|
|
||||||
|
@ -222,3 +221,29 @@ def test_add_contact(mega):
|
||||||
def test_remove_contact(mega):
|
def test_remove_contact(mega):
|
||||||
resp = mega.remove_contact(TEST_CONTACT)
|
resp = mega.remove_contact(TEST_CONTACT)
|
||||||
assert isinstance(resp, int)
|
assert isinstance(resp, int)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('url, expected_file_id_and_key', [
|
||||||
|
('https://mega.nz/#!Ue5VRSIQ!kC2E4a4JwfWWCWYNJovGFHlbz8F'
|
||||||
|
'N-ISsBAGPzvTjT6k',
|
||||||
|
'Ue5VRSIQ!kC2E4a4JwfWWCWYNJovGFHlbz8FN-ISsBAGPzvTjT6k'),
|
||||||
|
('https://mega.nz/file/cH51DYDR#qH7QOfRcM-7N9riZWdSjsRq'
|
||||||
|
'5VDTLfIhThx1capgVA30',
|
||||||
|
'cH51DYDR!qH7QOfRcM-7N9riZWdSjsRq5VDTLfIhThx1capgVA30'),
|
||||||
|
])
|
||||||
|
def test_parse_url(url, expected_file_id_and_key, mega):
|
||||||
|
assert mega._parse_url(url) == expected_file_id_and_key
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
|
class TestAPIRequest:
|
||||||
|
@pytest.mark.parametrize('response_text', ['-3', '-9'])
|
||||||
|
def test_when_api_returns_int_raises_exception(
|
||||||
|
self,
|
||||||
|
mega,
|
||||||
|
response_text,
|
||||||
|
):
|
||||||
|
with requests_mock.Mocker() as m:
|
||||||
|
m.post(f'{mega.schema}://g.api.{mega.domain}/cs',
|
||||||
|
text=response_text)
|
||||||
|
mega._api_request(data={})
|
||||||
|
|
Loading…
Reference in a new issue