various fixes
This commit is contained in:
parent
52471551a8
commit
00c2fb2192
12 changed files with 390 additions and 203 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,3 +10,4 @@ build/
|
||||||
dist/
|
dist/
|
||||||
.pypirc
|
.pypirc
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
.vscode/
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "2.7"
|
- "2.7"
|
||||||
install: pip install -r requirements.txt
|
install: pip install -r requirements-dev.txt
|
||||||
script: python tests/unit-tests.py
|
script: python setup.py install && pytest tests/unit-tests.py -x -s -v
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
from mega import Mega
|
from mega import Mega
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,8 +10,8 @@ def test():
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# user details
|
# user details
|
||||||
email = 'your@email.com'
|
email = os.environ['EMAIL']
|
||||||
password = 'password'
|
password = os.environ['PASS']
|
||||||
|
|
||||||
mega = Mega()
|
mega = Mega()
|
||||||
# mega = Mega({'verbose': True}) # verbose option for print output
|
# mega = Mega({'verbose': True}) # verbose option for print output
|
||||||
|
@ -58,5 +59,6 @@ def test():
|
||||||
# empty trash
|
# empty trash
|
||||||
print(m.empty_trash())
|
print(m.empty_trash())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
test()
|
test()
|
|
@ -1 +1 @@
|
||||||
from .mega import Mega
|
from .mega import Mega # noqa
|
||||||
|
|
|
@ -48,14 +48,14 @@ 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):
|
||||||
|
|
|
@ -11,4 +11,3 @@ class RequestError(Exception):
|
||||||
"""
|
"""
|
||||||
# TODO add error response messages
|
# TODO add error response messages
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
342
mega/mega.py
342
mega/mega.py
|
@ -9,7 +9,11 @@ import binascii
|
||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
from .errors import ValidationError, RequestError
|
from .errors import ValidationError, RequestError
|
||||||
from .crypto import *
|
from .crypto import (
|
||||||
|
a32_to_base64, encrypt_key, base64_url_encode, encrypt_attr, base64_to_a32,
|
||||||
|
base64_url_decode, decrypt_attr, a32_to_str, get_chunks, str_to_a32,
|
||||||
|
decrypt_key, mpi_to_int, stringhash, prepare_key, make_id,
|
||||||
|
)
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +21,7 @@ class Mega(object):
|
||||||
def __init__(self, options=None):
|
def __init__(self, options=None):
|
||||||
self.schema = 'https'
|
self.schema = 'https'
|
||||||
self.domain = 'mega.co.nz'
|
self.domain = 'mega.co.nz'
|
||||||
self.timeout = 160 # max time (secs) to wait for resp from api requests
|
self.timeout = 160 # max secs to wait for resp from api requests
|
||||||
self.sid = None
|
self.sid = None
|
||||||
self.sequence_num = random.randint(0, 0xFFFFFFFF)
|
self.sequence_num = random.randint(0, 0xFFFFFFFF)
|
||||||
self.request_id = make_id(10)
|
self.request_id = make_id(10)
|
||||||
|
@ -47,12 +51,20 @@ class Mega(object):
|
||||||
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': 'up',
|
{
|
||||||
'k': a32_to_base64(encrypt_key(master_key, password_key)),
|
'a':
|
||||||
'ts': base64_url_encode(a32_to_str(session_self_challenge) +
|
'up',
|
||||||
a32_to_str(encrypt_key(session_self_challenge, master_key)))
|
'k':
|
||||||
})
|
a32_to_base64(encrypt_key(master_key, password_key)),
|
||||||
|
'ts':
|
||||||
|
base64_url_encode(
|
||||||
|
a32_to_str(session_self_challenge) + a32_to_str(
|
||||||
|
encrypt_key(session_self_challenge, master_key)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
resp = self._api_request({'a': 'us', 'user': user})
|
resp = self._api_request({'a': 'us', 'user': user})
|
||||||
# if numeric error code response
|
# if numeric error code response
|
||||||
|
@ -66,27 +78,34 @@ class Mega(object):
|
||||||
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(encrypted_rsa_private_key,
|
rsa_private_key = decrypt_key(
|
||||||
self.master_key)
|
encrypted_rsa_private_key, self.master_key
|
||||||
|
)
|
||||||
|
|
||||||
private_key = a32_to_str(rsa_private_key)
|
private_key = a32_to_str(rsa_private_key)
|
||||||
self.rsa_private_key = [0, 0, 0, 0]
|
self.rsa_private_key = [0, 0, 0, 0]
|
||||||
|
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
l = ((ord(private_key[0]) * 256 + ord(private_key[1]) + 7) / 8) + 2
|
l = (
|
||||||
|
(ord(private_key[0]) * 256 + ord(private_key[1]) + 7) / 8
|
||||||
|
) + 2
|
||||||
self.rsa_private_key[i] = mpi_to_int(private_key[:l])
|
self.rsa_private_key[i] = mpi_to_int(private_key[:l])
|
||||||
private_key = private_key[l:]
|
private_key = private_key[l:]
|
||||||
|
|
||||||
encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
|
encrypted_sid = mpi_to_int(base64_url_decode(resp['csid']))
|
||||||
rsa_decrypter = RSA.construct(
|
rsa_decrypter = RSA.construct(
|
||||||
(self.rsa_private_key[0] * self.rsa_private_key[1],
|
(
|
||||||
0L, self.rsa_private_key[2], self.rsa_private_key[0],
|
self.rsa_private_key[0] * self.rsa_private_key[1], 0L,
|
||||||
self.rsa_private_key[1]))
|
self.rsa_private_key[2], self.rsa_private_key[0],
|
||||||
|
self.rsa_private_key[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
sid = '%x' % rsa_decrypter.key._decrypt(encrypted_sid)
|
sid = '%x' % rsa_decrypter.key._decrypt(encrypted_sid)
|
||||||
sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid)
|
sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid)
|
||||||
|
@ -103,11 +122,23 @@ class Mega(object):
|
||||||
if not isinstance(data, list):
|
if not isinstance(data, list):
|
||||||
data = [data]
|
data = [data]
|
||||||
|
|
||||||
|
url = '{0}://g.api.{1}/cs'.format(self.schema, self.domain)
|
||||||
req = requests.post(
|
req = requests.post(
|
||||||
'{0}://g.api.{1}/cs'.format(self.schema, self.domain),
|
url,
|
||||||
params=params,
|
params=params,
|
||||||
data=json.dumps(data),
|
data=json.dumps(data),
|
||||||
timeout=self.timeout)
|
timeout=self.timeout,
|
||||||
|
headers={
|
||||||
|
'Origin':
|
||||||
|
'https://mega.nz',
|
||||||
|
'Referer':
|
||||||
|
'https://mega.nz/login',
|
||||||
|
'User-Agent': (
|
||||||
|
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:69.0) '
|
||||||
|
'Gecko/20100101 Firefox/69.0'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
json_resp = json.loads(req.text)
|
json_resp = json.loads(req.text)
|
||||||
|
|
||||||
# if numeric error code response
|
# if numeric error code response
|
||||||
|
@ -129,7 +160,11 @@ class Mega(object):
|
||||||
Process a file
|
Process a file
|
||||||
"""
|
"""
|
||||||
if file['t'] == 0 or file['t'] == 1:
|
if file['t'] == 0 or file['t'] == 1:
|
||||||
keys = dict(keypart.split(':', 1) for keypart in file['k'].split('/') if ':' in keypart)
|
keys = dict(
|
||||||
|
keypart.split(':', 1)
|
||||||
|
for keypart in file['k'].split('/')
|
||||||
|
if ':' in keypart
|
||||||
|
)
|
||||||
uid = file['u']
|
uid = file['u']
|
||||||
key = None
|
key = None
|
||||||
# my objects
|
# my objects
|
||||||
|
@ -137,7 +172,9 @@ class Mega(object):
|
||||||
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(base64_to_a32(file['sk']), self.master_key)
|
shared_key = decrypt_key(
|
||||||
|
base64_to_a32(file['sk']), 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']] = {}
|
||||||
|
@ -153,8 +190,10 @@ class Mega(object):
|
||||||
if key is not None:
|
if key is not None:
|
||||||
# file
|
# file
|
||||||
if file['t'] == 0:
|
if file['t'] == 0:
|
||||||
k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6],
|
k = (
|
||||||
key[3] ^ key[7])
|
key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6],
|
||||||
|
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
|
||||||
|
@ -189,7 +228,9 @@ class Mega(object):
|
||||||
"""
|
"""
|
||||||
ok_dict = {}
|
ok_dict = {}
|
||||||
for ok_item in files['ok']:
|
for ok_item in files['ok']:
|
||||||
shared_key = decrypt_key(base64_to_a32(ok_item['k']), self.master_key)
|
shared_key = decrypt_key(
|
||||||
|
base64_to_a32(ok_item['k']), 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:
|
||||||
|
@ -233,6 +274,8 @@ class Mega(object):
|
||||||
"""
|
"""
|
||||||
files = self.get_files()
|
files = self.get_files()
|
||||||
for file in files.items():
|
for file in files.items():
|
||||||
|
if not isinstance(file[1]['a'], dict):
|
||||||
|
continue
|
||||||
if file[1]['a'] and file[1]['a']['n'] == filename:
|
if file[1]['a'] and file[1]['a']['n'] == filename:
|
||||||
return file
|
return file
|
||||||
|
|
||||||
|
@ -260,15 +303,17 @@ class Mega(object):
|
||||||
file = file['f'][0]
|
file = file['f'][0]
|
||||||
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(decrypt_key(base64_to_a32(file_key),
|
decrypted_key = a32_to_base64(
|
||||||
self.master_key))
|
decrypt_key(base64_to_a32(file_key), self.master_key)
|
||||||
return '{0}://{1}/#!{2}!{3}'.format(self.schema,
|
)
|
||||||
self.domain,
|
return '{0}://{1}/#!{2}!{3}'.format(
|
||||||
public_handle,
|
self.schema, self.domain, public_handle, decrypted_key
|
||||||
decrypted_key)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError('''Upload() response required as input,
|
raise ValueError(
|
||||||
use get_link() for regular file input''')
|
'''Upload() response required as input,
|
||||||
|
use get_link() for regular file input'''
|
||||||
|
)
|
||||||
|
|
||||||
def get_link(self, file):
|
def get_link(self, file):
|
||||||
"""
|
"""
|
||||||
|
@ -278,12 +323,14 @@ class Mega(object):
|
||||||
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("Can't get a public link from that file (is this a shared file?)")
|
raise RequestError(
|
||||||
|
"Can't get a public link from that file "
|
||||||
|
"(is this a shared file?)"
|
||||||
|
)
|
||||||
decrypted_key = a32_to_base64(file['key'])
|
decrypted_key = a32_to_base64(file['key'])
|
||||||
return '{0}://{1}/#!{2}!{3}'.format(self.schema,
|
return '{0}://{1}/#!{2}!{3}'.format(
|
||||||
self.domain,
|
self.schema, self.domain, public_handle, decrypted_key
|
||||||
public_handle,
|
)
|
||||||
decrypted_key)
|
|
||||||
else:
|
else:
|
||||||
raise ValidationError('File id and key must be present')
|
raise ValidationError('File id and key must be present')
|
||||||
|
|
||||||
|
@ -338,7 +385,7 @@ class Mega(object):
|
||||||
node_id = None
|
node_id = None
|
||||||
|
|
||||||
for i in node_data['f']:
|
for i in node_data['f']:
|
||||||
if i['h'] is not u'':
|
if i['h'] != u'':
|
||||||
node_id = i['h']
|
node_id = i['h']
|
||||||
return node_id
|
return node_id
|
||||||
|
|
||||||
|
@ -346,7 +393,14 @@ class Mega(object):
|
||||||
"""
|
"""
|
||||||
Get current remaining disk quota in MegaBytes
|
Get current remaining disk quota in MegaBytes
|
||||||
"""
|
"""
|
||||||
json_resp = self._api_request({'a': 'uq', 'xfer': 1})
|
json_resp = self._api_request(
|
||||||
|
{
|
||||||
|
'a': 'uq',
|
||||||
|
'xfer': 1,
|
||||||
|
'strg': 1,
|
||||||
|
'v': 1
|
||||||
|
}
|
||||||
|
)
|
||||||
# convert bytes to megabyes
|
# convert bytes to megabyes
|
||||||
return json_resp['mstrg'] / 1048576
|
return json_resp['mstrg'] / 1048576
|
||||||
|
|
||||||
|
@ -402,9 +456,13 @@ class Mega(object):
|
||||||
"""
|
"""
|
||||||
Destroy a file by its private id
|
Destroy a file by its private id
|
||||||
"""
|
"""
|
||||||
return self._api_request({'a': 'd',
|
return self._api_request(
|
||||||
|
{
|
||||||
|
'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):
|
||||||
"""
|
"""
|
||||||
|
@ -423,9 +481,7 @@ class Mega(object):
|
||||||
if files != {}:
|
if files != {}:
|
||||||
post_list = []
|
post_list = []
|
||||||
for file in files:
|
for file in files:
|
||||||
post_list.append({"a": "d",
|
post_list.append({"a": "d", "n": file, "i": self.request_id})
|
||||||
"n": file,
|
|
||||||
"i": self.request_id})
|
|
||||||
return self._api_request(post_list)
|
return self._api_request(post_list)
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -434,7 +490,14 @@ class Mega(object):
|
||||||
"""
|
"""
|
||||||
Download a file by it's file object
|
Download a file by it's file object
|
||||||
"""
|
"""
|
||||||
self._download_file(None, None, file=file[1], dest_path=dest_path, dest_filename=dest_filename, is_public=False)
|
self._download_file(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
file=file[1],
|
||||||
|
dest_path=dest_path,
|
||||||
|
dest_filename=dest_filename,
|
||||||
|
is_public=False
|
||||||
|
)
|
||||||
|
|
||||||
def download_url(self, url, dest_path=None, dest_filename=None):
|
def download_url(self, url, dest_path=None, dest_filename=None):
|
||||||
"""
|
"""
|
||||||
|
@ -443,18 +506,42 @@ class Mega(object):
|
||||||
path = self._parse_url(url).split('!')
|
path = self._parse_url(url).split('!')
|
||||||
file_id = path[0]
|
file_id = path[0]
|
||||||
file_key = path[1]
|
file_key = path[1]
|
||||||
self._download_file(file_id, file_key, dest_path, dest_filename, is_public=True)
|
self._download_file(
|
||||||
|
file_id, file_key, dest_path, dest_filename, is_public=True
|
||||||
|
)
|
||||||
|
|
||||||
def _download_file(self, file_handle, file_key, dest_path=None, dest_filename=None, is_public=False, file=None):
|
def _download_file(
|
||||||
|
self,
|
||||||
|
file_handle,
|
||||||
|
file_key,
|
||||||
|
dest_path=None,
|
||||||
|
dest_filename=None,
|
||||||
|
is_public=False,
|
||||||
|
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({'a': 'g', 'g': 1, 'p': file_handle})
|
file_data = self._api_request(
|
||||||
|
{
|
||||||
|
'a': 'g',
|
||||||
|
'g': 1,
|
||||||
|
'p': file_handle
|
||||||
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
file_data = self._api_request({'a': 'g', 'g': 1, 'n': file_handle})
|
file_data = self._api_request(
|
||||||
|
{
|
||||||
|
'a': 'g',
|
||||||
|
'g': 1,
|
||||||
|
'n': file_handle
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
k = (file_key[0] ^ file_key[4], file_key[1] ^ file_key[5],
|
k = (
|
||||||
file_key[2] ^ file_key[6], file_key[3] ^ file_key[7])
|
file_key[0] ^ file_key[4], file_key[1] ^ file_key[5],
|
||||||
|
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:
|
||||||
|
@ -485,11 +572,12 @@ class Mega(object):
|
||||||
else:
|
else:
|
||||||
dest_path += '/'
|
dest_path += '/'
|
||||||
|
|
||||||
temp_output_file = tempfile.NamedTemporaryFile(mode='w+b', prefix='megapy_', delete=False)
|
temp_output_file = tempfile.NamedTemporaryFile(
|
||||||
|
mode='w+b', prefix='megapy_', delete=False
|
||||||
|
)
|
||||||
|
|
||||||
k_str = a32_to_str(k)
|
k_str = a32_to_str(k)
|
||||||
counter = Counter.new(
|
counter = Counter.new(128, initial_value=((iv[0] << 32) + iv[1]) << 64)
|
||||||
128, 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
|
||||||
|
@ -520,7 +608,11 @@ class Mega(object):
|
||||||
if self.options.get('verbose') is True:
|
if self.options.get('verbose') is True:
|
||||||
# temp file size
|
# temp file size
|
||||||
file_info = os.stat(temp_output_file.name)
|
file_info = os.stat(temp_output_file.name)
|
||||||
print('{0} of {1} downloaded'.format(file_info.st_size, file_size))
|
print(
|
||||||
|
'{0} of {1} downloaded'.format(
|
||||||
|
file_info.st_size, file_size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
file_mac = str_to_a32(mac_str)
|
file_mac = str_to_a32(mac_str)
|
||||||
|
|
||||||
|
@ -550,7 +642,9 @@ class Mega(object):
|
||||||
# generate random aes key (128) for file
|
# generate random aes key (128) for file
|
||||||
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(128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64)
|
count = Counter.new(
|
||||||
|
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
|
||||||
|
@ -582,16 +676,24 @@ class Mega(object):
|
||||||
|
|
||||||
# encrypt file and upload
|
# encrypt file and upload
|
||||||
chunk = aes.encrypt(chunk)
|
chunk = aes.encrypt(chunk)
|
||||||
output_file = requests.post(ul_url + "/" + str(chunk_start),
|
output_file = requests.post(
|
||||||
data=chunk, timeout=self.timeout)
|
ul_url + "/" + str(chunk_start),
|
||||||
|
data=chunk,
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
completion_file_handle = output_file.text
|
completion_file_handle = output_file.text
|
||||||
|
|
||||||
if self.options.get('verbose') is True:
|
if self.options.get('verbose') is True:
|
||||||
# upload progress
|
# upload progress
|
||||||
print('{0} of {1} uploaded'.format(upload_progress, file_size))
|
print(
|
||||||
|
'{0} of {1} uploaded'.format(
|
||||||
|
upload_progress, file_size
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
output_file = requests.post(ul_url + "/0",
|
output_file = requests.post(
|
||||||
data='', timeout=self.timeout)
|
ul_url + "/0", data='', timeout=self.timeout
|
||||||
|
)
|
||||||
completion_file_handle = output_file.text
|
completion_file_handle = output_file.text
|
||||||
|
|
||||||
file_mac = str_to_a32(mac_str)
|
file_mac = str_to_a32(mac_str)
|
||||||
|
@ -605,22 +707,33 @@ class Mega(object):
|
||||||
attribs = {'n': os.path.basename(filename)}
|
attribs = {'n': os.path.basename(filename)}
|
||||||
|
|
||||||
encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4]))
|
encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4]))
|
||||||
key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5],
|
key = [
|
||||||
ul_key[2] ^ meta_mac[0], ul_key[3] ^ meta_mac[1],
|
ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5],
|
||||||
ul_key[4], ul_key[5], meta_mac[0], meta_mac[1]]
|
ul_key[2] ^ meta_mac[0], ul_key[3] ^ meta_mac[1], ul_key[4],
|
||||||
|
ul_key[5], meta_mac[0], meta_mac[1]
|
||||||
|
]
|
||||||
encrypted_key = a32_to_base64(encrypt_key(key, self.master_key))
|
encrypted_key = a32_to_base64(encrypt_key(key, self.master_key))
|
||||||
# update attributes
|
# update attributes
|
||||||
data = self._api_request({'a': 'p', 't': dest, 'n': [{
|
data = self._api_request(
|
||||||
|
{
|
||||||
|
'a':
|
||||||
|
'p',
|
||||||
|
't':
|
||||||
|
dest,
|
||||||
|
'n': [
|
||||||
|
{
|
||||||
'h': completion_file_handle,
|
'h': completion_file_handle,
|
||||||
't': 0,
|
't': 0,
|
||||||
'a': encrypt_attribs,
|
'a': encrypt_attribs,
|
||||||
'k': encrypted_key}]})
|
'k': encrypted_key
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
# close input file and return API msg
|
# close input file and return API msg
|
||||||
input_file.close()
|
input_file.close()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
##########################################################################
|
|
||||||
# OTHER OPERATIONS
|
|
||||||
def create_folder(self, name, dest=None):
|
def create_folder(self, name, dest=None):
|
||||||
# determine storage node
|
# determine storage node
|
||||||
if dest is None:
|
if dest is None:
|
||||||
|
@ -638,16 +751,24 @@ class Mega(object):
|
||||||
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({'a': 'p',
|
data = self._api_request(
|
||||||
't': dest,
|
{
|
||||||
'n': [{
|
'a':
|
||||||
|
'p',
|
||||||
|
't':
|
||||||
|
dest,
|
||||||
|
'n': [
|
||||||
|
{
|
||||||
'h': 'xxxxxxxx',
|
'h': 'xxxxxxxx',
|
||||||
't': 1,
|
't': 1,
|
||||||
'a': encrypt_attribs,
|
'a': encrypt_attribs,
|
||||||
'k': encrypted_key}
|
'k': encrypted_key
|
||||||
|
}
|
||||||
],
|
],
|
||||||
'i': self.request_id})
|
'i':
|
||||||
#return API msg
|
self.request_id
|
||||||
|
}
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def rename(self, file, new_name):
|
def rename(self, file, new_name):
|
||||||
|
@ -656,17 +777,22 @@ class Mega(object):
|
||||||
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(encrypt_key(file['key'], self.master_key))
|
encrypted_key = a32_to_base64(
|
||||||
|
encrypt_key(file['key'], self.master_key)
|
||||||
|
)
|
||||||
|
|
||||||
# update attributes
|
# update attributes
|
||||||
data = self._api_request([{
|
data = self._api_request(
|
||||||
|
[
|
||||||
|
{
|
||||||
'a': 'a',
|
'a': 'a',
|
||||||
'attr': encrypt_attribs,
|
'attr': encrypt_attribs,
|
||||||
'key': encrypted_key,
|
'key': encrypted_key,
|
||||||
'n': file['h'],
|
'n': file['h'],
|
||||||
'i': self.request_id}])
|
'i': self.request_id
|
||||||
|
}
|
||||||
#return API msg
|
]
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def move(self, file_id, target):
|
def move(self, file_id, target):
|
||||||
|
@ -697,10 +823,14 @@ class Mega(object):
|
||||||
else:
|
else:
|
||||||
file = target[1]
|
file = target[1]
|
||||||
target_node_id = file['h']
|
target_node_id = file['h']
|
||||||
return self._api_request({'a': 'm',
|
return self._api_request(
|
||||||
|
{
|
||||||
|
'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):
|
||||||
"""
|
"""
|
||||||
|
@ -728,17 +858,20 @@ class Mega(object):
|
||||||
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({'a': 'ur',
|
return self._api_request(
|
||||||
|
{
|
||||||
|
'a': 'ur',
|
||||||
'u': email,
|
'u': email,
|
||||||
'l': l,
|
'l': l,
|
||||||
'i': self.request_id})
|
'i': self.request_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def get_contacts(self):
|
def get_contacts(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
# TODO implement this
|
# TODO implement this
|
||||||
# sn param below = maxaction var with function getsc() in mega.co.nz js
|
# sn param below = maxaction var with function getsc() in mega.co.nz js
|
||||||
# seens to be the 'sn' attrib of the previous request response...
|
# seens to be the 'sn' attrib of the previous request response...
|
||||||
# mega.co.nz js full source @ http://homepages.shu.ac.uk/~rjodwyer/mega-scripts-all.js
|
|
||||||
# requests goto /sc rather than
|
# requests goto /sc rather than
|
||||||
|
|
||||||
# req = requests.post(
|
# req = requests.post(
|
||||||
|
@ -761,16 +894,15 @@ class Mega(object):
|
||||||
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(file_handle, file_key, dest_node=dest_node, dest_name=dest_name)
|
return self.import_public_file(
|
||||||
|
file_handle, 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):
|
||||||
"""
|
"""
|
||||||
Get size and name of a public file
|
Get size and name of a public file
|
||||||
"""
|
"""
|
||||||
data = self._api_request({
|
data = self._api_request({'a': 'g', 'p': file_handle, 'ssm': 1})
|
||||||
'a': 'g',
|
|
||||||
'p': file_handle,
|
|
||||||
'ssm': 1})
|
|
||||||
|
|
||||||
# if numeric error code response
|
# if numeric error code response
|
||||||
if isinstance(data, int):
|
if isinstance(data, int):
|
||||||
|
@ -780,20 +912,22 @@ class Mega(object):
|
||||||
raise ValueError("Unexpected result", data)
|
raise ValueError("Unexpected result", data)
|
||||||
|
|
||||||
key = base64_to_a32(file_key)
|
key = base64_to_a32(file_key)
|
||||||
k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7])
|
k = (
|
||||||
|
key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], 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)
|
||||||
if not unencrypted_attrs:
|
if not unencrypted_attrs:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
result = {
|
result = {'size': size, 'name': unencrypted_attrs['n']}
|
||||||
'size': size,
|
|
||||||
'name': unencrypted_attrs['n']}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def import_public_file(self, file_handle, file_key, dest_node=None, dest_name=None):
|
def import_public_file(
|
||||||
|
self, file_handle, file_key, dest_node=None, dest_name=None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Import the public file into user account
|
Import the public file into user account
|
||||||
"""
|
"""
|
||||||
|
@ -809,19 +943,27 @@ class Mega(object):
|
||||||
dest_name = pl_info['name']
|
dest_name = pl_info['name']
|
||||||
|
|
||||||
key = base64_to_a32(file_key)
|
key = base64_to_a32(file_key)
|
||||||
k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7])
|
k = (
|
||||||
|
key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], 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))
|
||||||
|
|
||||||
data = self._api_request({
|
data = self._api_request(
|
||||||
'a': 'p',
|
{
|
||||||
't': dest_node['h'],
|
'a':
|
||||||
'n': [{
|
'p',
|
||||||
|
't':
|
||||||
|
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
|
||||||
|
}
|
||||||
#return API msg
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
8
requirements-dev.txt
Normal file
8
requirements-dev.txt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
-r requirements.txt
|
||||||
|
pytest
|
||||||
|
ipdb
|
||||||
|
flake8
|
||||||
|
pep8-naming
|
||||||
|
autoflake
|
||||||
|
mccabe
|
||||||
|
yapf
|
|
@ -1,3 +1,2 @@
|
||||||
requests>=0.10
|
requests>=0.10
|
||||||
pycrypto
|
pycrypto
|
||||||
mega.py
|
|
||||||
|
|
28
setup.cfg
Normal file
28
setup.cfg
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[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
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
23
setup.py
23
setup.py
|
@ -9,25 +9,31 @@ def get_packages(package):
|
||||||
"""
|
"""
|
||||||
Return root package & all sub-packages.
|
Return root package & all sub-packages.
|
||||||
"""
|
"""
|
||||||
return [dirpath
|
return [
|
||||||
for dirpath, dirnames, filenames in os.walk(package)
|
dirpath for dirpath, dirnames, filenames in os.walk(package)
|
||||||
if os.path.exists(os.path.join(dirpath, '__init__.py'))]
|
if os.path.exists(os.path.join(dirpath, '__init__.py'))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_package_data(package):
|
def get_package_data(package):
|
||||||
"""
|
"""
|
||||||
Return all files under the root package, that are not in a
|
Return all files under the root package, that are not in a
|
||||||
package themselves.
|
package themselves.
|
||||||
"""
|
"""
|
||||||
walk = [(dirpath.replace(package + os.sep, '', 1), filenames)
|
walk = [
|
||||||
|
(dirpath.replace(package + os.sep, '', 1), filenames)
|
||||||
for dirpath, dirnames, filenames in os.walk(package)
|
for dirpath, dirnames, filenames in os.walk(package)
|
||||||
if not os.path.exists(os.path.join(dirpath, '__init__.py'))]
|
if not os.path.exists(os.path.join(dirpath, '__init__.py'))
|
||||||
|
]
|
||||||
|
|
||||||
filepaths = []
|
filepaths = []
|
||||||
for base, filenames in walk:
|
for base, filenames in walk:
|
||||||
filepaths.extend([os.path.join(base, filename)
|
filepaths.extend(
|
||||||
for filename in filenames])
|
[os.path.join(base, filename) for filename in filenames]
|
||||||
|
)
|
||||||
return {package: filepaths}
|
return {package: filepaths}
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='mega.py',
|
name='mega.py',
|
||||||
version='0.9.17',
|
version='0.9.17',
|
||||||
|
@ -41,8 +47,7 @@ setup(
|
||||||
install_requires=['pycrypto', 'requests'],
|
install_requires=['pycrypto', 'requests'],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent', 'Programming Language :: Python',
|
||||||
'Programming Language :: Python',
|
|
||||||
'Topic :: Internet :: WWW/HTTP'
|
'Topic :: Internet :: WWW/HTTP'
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,13 +3,14 @@ These unit tests will upload a test file,a test folder and a test contact,
|
||||||
Perform api operations on them,
|
Perform api operations on them,
|
||||||
And them remove them from your account.
|
And them remove them from your account.
|
||||||
"""
|
"""
|
||||||
from mega import Mega
|
|
||||||
import unittest
|
import unittest
|
||||||
import random
|
import random
|
||||||
import os
|
import os
|
||||||
|
|
||||||
email = 'your@email.com'
|
from mega import Mega
|
||||||
password = 'password'
|
|
||||||
|
email = os.environ['EMAIL']
|
||||||
|
password = os.environ['PASS']
|
||||||
|
|
||||||
mega = Mega()
|
mega = Mega()
|
||||||
# anonymous login
|
# anonymous login
|
||||||
|
@ -19,7 +20,9 @@ m = mega.login()
|
||||||
|
|
||||||
FIND_RESP = None
|
FIND_RESP = None
|
||||||
TEST_CONTACT = 'test@mega.co.nz'
|
TEST_CONTACT = 'test@mega.co.nz'
|
||||||
TEST_PUBLIC_URL = 'https://mega.co.nz/#!EYI2VagT!Ic1yblki8oM4v6XHquCe4gu84kxc4glFchj8OvcT5lw'
|
TEST_PUBLIC_URL = (
|
||||||
|
'https://mega.nz/#!hYVmXKqL!r0d0-WRnFwulR_shhuEDwrY1Vo103-am1MyUy8oV6Ps'
|
||||||
|
)
|
||||||
TEST_FILE = os.path.basename(__file__)
|
TEST_FILE = os.path.basename(__file__)
|
||||||
TEST_FOLDER = 'mega.py_testfolder_{0}'.format(random.random())
|
TEST_FOLDER = 'mega.py_testfolder_{0}'.format(random.random())
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue