Commit 61c5c240 authored by richardARPANET's avatar richardARPANET

mark repo as moved

parent 631ca606
sudo: false
language: python
python:
- 3.6
- 3.7
- 3.8
env:
- TOXENV=py-normal
install: pip install tox
script: tox
Mega API information
=====================
This file contains definitions for some of the properties within the API. The aim of the file is that more people will contribute through understanding.
### Node attributes (json properties)
* 'a' Type
* 'h' Id
* 'p' Parent Id
* 'a' encrypted Attributes (within this: 'n' Name)
* 'k' Node Key
* 'u' User Id
* 's' Size
* 'ts' Time Stamp
#### Node types
* 0 File
* 1 Folder
* 2 Root Folder
* 3 Inbox
* 4 Trash
* -1 Dummy
### Error responses
#### General errors:
* EINTERNAL (-1):
* EARGS (-2):
* EAGAIN (-3)
* ERATELIMIT (-4):
#### Upload errors:
* EFAILED (-5):
* ETOOMANY (-6):
* ERANGE (-7):
* EEXPIRED (-8):
#### Filesystem/Account level errors:
* ENOENT (-9):
* ECIRCULAR (-10):
* EACCESS (-11):
* EEXIST (-12):
* EINCOMPLETE (-13):
* EKEY (-14):
* ESID (-15):
* EBLOCKED (-16):
* EOVERQUOTA (-17):
* ETEMPUNAVAIL (-18):
.. :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
This diff is collapsed.
include HISTORY.rst
include README.rst
include requirements.txt
recursive-include tests *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
recursive-include docs *.rst conf.py Makefile make.bat
# REPO MOVED
This repository has moved to https://github.com/odwyersoftware/mega.py
**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/
import os
from mega import Mega
def test():
"""
Enter your account details to begin
comment/uncomment lines to test various parts of the API
see readme.md for more information
"""
# user details
email = os.environ['EMAIL']
password = os.environ['PASS']
mega = Mega()
# mega = Mega({'verbose': True}) # verbose option for print output
# login
m = mega.login(email, password)
# get user details
details = m.get_user()
print(details)
# get account files
files = m.get_files()
# get account disk quota in MB
print((m.get_quota()))
# get account storage space
print((m.get_storage_space()))
# example iterate over files
for file in files:
print((files[file]))
# upload file
print((m.upload('examples.py')))
# search for a file in account
file = m.find('examples.py')
if file:
# get public link
link = m.get_link(file)
print(link)
# download file. by file object or url
print(m.download(file, '/tmp'))
# m.download_url(link)
# delete or destroy file. by id or url
print((m.delete(file[0])))
# print(m.destroy(file[0]))
# print(m.delete_url(link))
# print(m.destroy_url(link))
# empty trash
print((m.empty_trash()))
if __name__ == '__main__':
test()
-r requirements.txt
pytest
ipdb
flake8
pep8-naming
autoflake
mccabe
yapf
tox
coverage
pytest-cov
zest.releaser
setuptools
twine
wheel
rope
pytest-mock
requests>=0.10
pycryptodome>=3.9.6,<4.0.0
pathlib==1.0.1
tenacity>=5.1.5,<6.0.0
[bdist_wheel]
universal = 1
[zest.releaser]
create-wheel = yes
[tool:pytest]
addopts = -x -s -v
norecursedirs = .git
[flake8]
exclude = .git,__pycache__,legacy,build,dist,.tox
max-complexity = 15
ignore = E741,W504,W503
[yapf]
based_on_style = pep8
spaces_before_comment = 2
split_before_logical_operator = 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]
omit =
tests/*
src/*
setup.py
.tox/*
dist/*
**/celeryconfig.py
**/constants.py
**/migrations/*
**/__init__.py
app.py
**/env.py
**/urls.py
#!/usr/bin/env python
# -*- coding: utf-8 -*
from __future__ import absolute_import
import os
from codecs import open
from setuptools import find_packages, setup
# allow setup.py to be run from any path
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
with open('requirements.txt') as f:
install_requires = f.read().splitlines()
with open('README.rst', 'r', encoding='utf-8') as rm_file:
readme = rm_file.read()
with open('HISTORY.rst', 'r', encoding='utf-8') as hist_file:
history = hist_file.read()
setup(
name='mega.py',
version='1.0.7',
packages=find_packages('src', exclude=('tests', )),
package_dir={'': 'src'},
include_package_data=True,
zip_safe=False,
description='Python lib for the Mega.co.nz API',
long_description=readme + '\n\n' + history,
author='Richard O\'Dwyer',
author_email='richard@richard.do',
license='Creative Commons Attribution-Noncommercial-Share Alike license',
install_requires=install_requires,
classifiers=[
'Intended Audience :: Developers',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Internet :: WWW/HTTP',
]
)
from .mega import Mega # noqa
from Crypto.Cipher import AES
import json
import base64
import struct
import binascii
import random
import sys
# Python3 compatibility
if sys.version_info < (3, ):
def makebyte(x):
return x
def makestring(x):
return x
else:
import codecs
def makebyte(x):
return codecs.latin_1_encode(x)[0]
def makestring(x):
return codecs.latin_1_decode(x)[0]
def aes_cbc_encrypt(data, key):
aes_cipher = AES.new(key, AES.MODE_CBC, makebyte('\0' * 16))
return aes_cipher.encrypt(data)
def aes_cbc_decrypt(data, key):
aes_cipher = AES.new(key, AES.MODE_CBC, makebyte('\0' * 16))
return aes_cipher.decrypt(data)
def aes_cbc_encrypt_a32(data, key):
return str_to_a32(aes_cbc_encrypt(a32_to_str(data), a32_to_str(key)))
def aes_cbc_decrypt_a32(data, key):
return str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key)))
def stringhash(str, aeskey):
s32 = str_to_a32(str)
h32 = [0, 0, 0, 0]
for i in range(len(s32)):
h32[i % 4] ^= s32[i]
for r in range(0x4000):
h32 = aes_cbc_encrypt_a32(h32, aeskey)
return a32_to_base64((h32[0], h32[2]))
def prepare_key(arr):
pkey = [0x93C467E3, 0x7DB0C7A4, 0xD1BE3F81, 0x0152CB56]
for r in range(0x10000):
for j in range(0, len(arr), 4):
key = [0, 0, 0, 0]
for i in range(4):
if i + j < len(arr):
key[i] = arr[i + j]
pkey = aes_cbc_encrypt_a32(pkey, key)
return pkey
def encrypt_key(a, key):
return sum(
(aes_cbc_encrypt_a32(a[i:i + 4], key) for i in range(0, len(a), 4)), ()
)
def decrypt_key(a, key):
return sum(
(aes_cbc_decrypt_a32(a[i:i + 4], key) for i in range(0, len(a), 4)), ()
)
def encrypt_attr(attr, key):
attr = makebyte('MEGA' + json.dumps(attr))
if len(attr) % 16:
attr += b'\0' * (16 - len(attr) % 16)
return aes_cbc_encrypt(attr, a32_to_str(key))
def decrypt_attr(attr, key):
attr = aes_cbc_decrypt(attr, a32_to_str(key))
attr = makestring(attr)
attr = attr.rstrip('\0')
return json.loads(attr[4:]) if attr[:6] == 'MEGA{"' else False
def a32_to_str(a):
return struct.pack('>%dI' % len(a), *a)
def str_to_a32(b):
if isinstance(b, str):
b = makebyte(b)
if len(b) % 4:
# pad to multiple of 4
b += b'\0' * (4 - len(b) % 4)
return struct.unpack('>%dI' % (len(b) / 4), b)
def mpi_to_int(s):
"""
A Multi-precision integer is encoded as a series of bytes in big-endian
order. The first two bytes are a header which tell the number of bits in
the integer. The rest of the bytes are the integer.
"""
return int(binascii.hexlify(s[2:]), 16)
def extended_gcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = extended_gcd(b % a, a)
return (g, x - (b // a) * y, y)
def modular_inverse(a, m):
g, x, y = extended_gcd(a, m)
if g != 1:
raise Exception('modular inverse does not exist')
else:
return x % m
def base64_url_decode(data):
data += '=='[(2 - len(data) * 3) % 4:]
for search, replace in (('-', '+'), ('_', '/'), (',', '')):
data = data.replace(search, replace)
return base64.b64decode(data)
def base64_to_a32(s):
return str_to_a32(base64_url_decode(s))
def base64_url_encode(data):
data = base64.b64encode(data)
data = makestring(data)
for search, replace in (('+', '-'), ('/', '_'), ('=', '')):
data = data.replace(search, replace)
return data
def a32_to_base64(a):
return base64_url_encode(a32_to_str(a))
def get_chunks(size):
p = 0
s = 0x20000
while p + s < size:
yield (p, s)
p += s
if s < 0x100000:
s += 0x20000
yield (p, size - p)
def make_id(length):
text = ''
possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for i in range(length):
text += random.choice(possible)
return text
class ValidationError(Exception):
"""
Error in validation stage
"""
pass
_CODE_TO_DESCRIPTIONS = {
-1: (
'EINTERNAL',
(
'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'),
-3: (
'EAGAIN',
(
'(always at the request level) A temporary congestion or server '
'malfunction prevented your request from being processed. '
'No data was altered. Retry. Retries must be spaced with '
'exponential backoff'
)
),
-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'),
-6: (
'ETOOMANY',
'Too many concurrent IP addresses are accessing this upload target URL'
),
-7: (
'ERANGE',
(
'The upload file packet is out of range or not starting and '
'ending on a chunk boundary'
)
),
-8: (
'EEXPIRED',
(
'The upload target URL you are trying to access has expired. '
'Please request a fresh one'
)