From ea50dfc90f7a377857b3450e8b34aed2ce64687c Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 5 Feb 2013 07:11:15 +0000 Subject: [PATCH 001/130] Added d/l by id+key --- README.md | 4 ++-- mega.py | 3 +++ tests/test.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b78a73..39d9e7b 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ This is a work in progress, further functionality coming shortly. m.upload('myfile.doc') -### Download a file from URL - +### Download a file from URL or it's ID,key combo + m.download('utYjgSTQ','OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') ### Trash a file from URL or it's ID diff --git a/mega.py b/mega.py index 24e1d90..c9a8386 100644 --- a/mega.py +++ b/mega.py @@ -97,6 +97,9 @@ def download_url(self, url): file_key = path[1] self.download_file(file_id, file_key, is_public=True) + def download(self, file_id, file_key): + self.download_file(file_id, file_key, is_public=True) + def parse_url(self, url): #parse file id and key from url if('!' in url): diff --git a/tests/test.py b/tests/test.py index fe22c28..8e8fcc3 100644 --- a/tests/test.py +++ b/tests/test.py @@ -25,7 +25,8 @@ def test(): #print(m.delete('C5pxAbr')) #print(m.delete_url('https://mega.co.nz/#!C5pxAbrL!SPxZH0Ovn2DLK_n5hLlkGQ2oTD8HcU6TYiz_TPg78kY')) - #download file from url + #download file, by id+key or url + m.download('6hBW0R4a','By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') if __name__ == '__main__': From 07a5248fa129c65486496c8073a8a0af4690994d Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 5 Feb 2013 07:45:03 +0000 Subject: [PATCH 002/130] Improved dest_id upload() check --- mega.py | 5 +++-- tests/test.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mega.py b/mega.py index c9a8386..8be2318 100644 --- a/mega.py +++ b/mega.py @@ -239,8 +239,9 @@ def upload(self, filename, dest=None): #determine storage node if dest is None: #if none set, upload to cloud drive node - root_id = getattr(self, 'root_id') - if root_id is None: + if hasattr(self, 'root_id'): + root_id = getattr(self, 'root_id') + else: self.get_files() dest = self.root_id diff --git a/tests/test.py b/tests/test.py index 8e8fcc3..9d1ee40 100644 --- a/tests/test.py +++ b/tests/test.py @@ -22,11 +22,11 @@ def test(): print(m.upload('test.py')) #trash a file, by id or url - #print(m.delete('C5pxAbr')) - #print(m.delete_url('https://mega.co.nz/#!C5pxAbrL!SPxZH0Ovn2DLK_n5hLlkGQ2oTD8HcU6TYiz_TPg78kY')) + #print(m.delete('f14U0JhD')) + #print(m.delete_url('https://mega.co.nz/#!f14U0JhD!S_2k-EvB5U1N3s0vm3I5C0JN2toHSGkVf0UxQsiKZ8A')) #download file, by id+key or url - m.download('6hBW0R4a','By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') + #m.download('6hBW0R4a','By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') if __name__ == '__main__': From 0d1acf6e999fd2f14057617f858ea093c32b8566 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Wed, 6 Feb 2013 12:35:56 +0000 Subject: [PATCH 003/130] Initial commit --- CONTRIBUTORS | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 CONTRIBUTORS diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..468af56 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,3 @@ +cyberjujum +richardasaurus +issues: zbahoui,alyssarowan From 321aa6524bdc42a2853e88232b0ae0819b803a06 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Wed, 6 Feb 2013 15:55:29 +0000 Subject: [PATCH 004/130] Removed dupe functions in crpyto, input file close, added example file output to test.py --- crypto.py | 16 ++-------------- mega.py | 3 +++ tests/test.py | 5 ++++- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/crypto.py b/crypto.py index 977c4a8..075ec1d 100644 --- a/crypto.py +++ b/crypto.py @@ -61,18 +61,12 @@ def encrypt_attr(attr, key): def decrypt_attr(attr, key): attr = aes_cbc_decrypt(attr, a32_to_str(key)).rstrip('\0') - return json.loads(attr[4:]) + return json.loads(attr[4:]) if attr[:6] == 'MEGA{"' else False def a32_to_str(a): return struct.pack('>%dI' % len(a), *a) -def aes_cbc_encrypt(data, key): - aes = AES.new(key, AES.MODE_CBC, '\0' * 16) - return aes.encrypt(data) - -def aes_cbc_encrypt_a32(data, key): - return str_to_a32(aes_cbc_encrypt(a32_to_str(data), a32_to_str(key))) def str_to_a32(b): if len(b) % 4: @@ -83,12 +77,6 @@ def str_to_a32(b): def mpi_to_int(s): return int(binascii.hexlify(s[2:]), 16) -def aes_cbc_decrypt(data, key): - decryptor = AES.new(key, AES.MODE_CBC, '\0' * 16) - return decryptor.decrypt(data) - -def aes_cbc_decrypt_a32(data, key): - return str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key))) def base64_url_decode(data): data += '=='[(2 - len(data) * 3) % 4:] @@ -136,4 +124,4 @@ def make_id(length): possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" for i in range(length): text += random.choice(possible) - return text \ No newline at end of file + return text diff --git a/mega.py b/mega.py index 8be2318..59b29db 100644 --- a/mega.py +++ b/mega.py @@ -292,6 +292,9 @@ def upload(self, filename, dest=None): data = self.api_request({'a': 'p', 't': dest, 'n': [ {'h': completion_file_handle, 't': 0, 'a': encrypt_attribs, 'k': encrypted_key}]} ) + + #close input file and return API msg + input_file.close() return data def process_file(self, file): diff --git a/tests/test.py b/tests/test.py index 9d1ee40..1e123bb 100644 --- a/tests/test.py +++ b/tests/test.py @@ -16,7 +16,10 @@ def test(): #get account files files = m.get_files() - print(files) + #example iterate over files + for file in files: + if files[file]['a'] != False: + print files[file] #upload file print(m.upload('test.py')) From bbc574e485934275827f1d8019ec36fcefdab8de Mon Sep 17 00:00:00 2001 From: richardasaurus Date: Thu, 7 Feb 2013 16:10:26 +0000 Subject: [PATCH 005/130] Added PyCrypto --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 39d9e7b..3af3781 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ This is a work in progress, further functionality coming shortly. 1. Python2.7+ 2. Python requests - python-requests.org + 3. PyCrypto - dlitz.net/software/pycrypto/ ## Tests From 1eab67d62884e286d3ded05836608e2e7ac45ab6 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 7 Feb 2013 18:06:03 +0000 Subject: [PATCH 006/130] Added file search function and get public link function --- README.md | 6 ++++++ mega.py | 17 +++++++++++++++++ tests/test.py | 26 +++++++++++++++++++------- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 39d9e7b..5070b17 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ This is a work in progress, further functionality coming shortly. m.download('utYjgSTQ','OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') +### Get a public link for a file + m.get_link('utYjgSTQ','OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') + +### Search for a file + m.find('myfile.doc') + ### Trash a file from URL or it's ID m.delete('utYjgSTQ') diff --git a/mega.py b/mega.py index 59b29db..8b48dd4 100644 --- a/mega.py +++ b/mega.py @@ -91,6 +91,12 @@ def get_files(self): files_dict[file['h']] = self.process_file(file) return files_dict + def get_link(self, file_id, file_key): + if file_id and file_key: + return '{0}://{1}#!{2}!{3}'.format(self.schema, self.domain, file_id, file_key) + else: + raise errors.RequestError('Url key missing') + def download_url(self, url): path = self.parse_url(url).split('!') file_id = path[0] @@ -123,6 +129,17 @@ def delete(self, file_id): #straight delete by id return self.move(file_id, 4) + def find(self, filename): + ''' + Return file object from given filename + ''' + files = self.get_files() + for file in files.items(): + if file[1]['a'] and file[1]['a']['n'] == filename: + return file + + + def move(self, file_id, target): #TODO node_id improvements ''' diff --git a/tests/test.py b/tests/test.py index 1e123bb..cd6ba17 100644 --- a/tests/test.py +++ b/tests/test.py @@ -7,30 +7,42 @@ def test(): mega = Mega() - #login + ##login m = mega.login(email, password) - #get user details + ##get user details details = m.get_user() print(details) - #get account files + ##get account files files = m.get_files() #example iterate over files for file in files: if files[file]['a'] != False: print files[file] - #upload file + ##upload file print(m.upload('test.py')) - #trash a file, by id or url + ##trash a file, by id or url #print(m.delete('f14U0JhD')) #print(m.delete_url('https://mega.co.nz/#!f14U0JhD!S_2k-EvB5U1N3s0vm3I5C0JN2toHSGkVf0UxQsiKZ8A')) - #download file, by id+key or url + ##search for a file on mega + files = m.find('test.py') + + if files: + #trash a file by it's id + #iterate to trash multiple results + print(m.delete(files[1]['k'])) + + + ##download file, by id+key or url #m.download('6hBW0R4a','By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') - m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') + #m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') + + ##get a public link for a file + print(m.get_link('6hBW0R4a','By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00')) if __name__ == '__main__': test() \ No newline at end of file From a26a6e2eae57253de40f40a2220d8c1684cd9809 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 7 Feb 2013 18:08:45 +0000 Subject: [PATCH 007/130] Added file search function and get public link function --- CONTRIBUTORS | 1 + mega.py | 2 -- tests/test.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 468af56..9b87731 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,3 +1,4 @@ cyberjujum richardasaurus +jlejeune issues: zbahoui,alyssarowan diff --git a/mega.py b/mega.py index 8b48dd4..988571b 100644 --- a/mega.py +++ b/mega.py @@ -138,8 +138,6 @@ def find(self, filename): if file[1]['a'] and file[1]['a']['n'] == filename: return file - - def move(self, file_id, target): #TODO node_id improvements ''' diff --git a/tests/test.py b/tests/test.py index cd6ba17..e549985 100644 --- a/tests/test.py +++ b/tests/test.py @@ -30,13 +30,11 @@ def test(): ##search for a file on mega files = m.find('test.py') - if files: #trash a file by it's id #iterate to trash multiple results print(m.delete(files[1]['k'])) - ##download file, by id+key or url #m.download('6hBW0R4a','By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') #m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') From 50e565e0bbe4e3d12f0509e450cfeb1100c41eb5 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 7 Feb 2013 18:12:45 +0000 Subject: [PATCH 008/130] Added delete by search example --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b326d5a..74bc34d 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,14 @@ This is a work in progress, further functionality coming shortly. ### Search for a file m.find('myfile.doc') -### Trash a file from URL or it's ID +### Trash a file from URL, it's ID, or from search m.delete('utYjgSTQ') m.delete_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') + + files = m.find('myfile.doc') + if files: + m.delete(files[1]['k']) ## Requirements From 7ed5bcee43900ece8ff193a876f073e38a878256 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 7 Feb 2013 18:29:16 +0000 Subject: [PATCH 009/130] Removed un-needed variables process_file() --- README.md | 2 +- mega.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 74bc34d..16a6d1d 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This is a work in progress, further functionality coming shortly. m.delete('utYjgSTQ') m.delete_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') - + files = m.find('myfile.doc') if files: m.delete(files[1]['k']) diff --git a/mega.py b/mega.py index 988571b..b07474f 100644 --- a/mega.py +++ b/mega.py @@ -321,8 +321,8 @@ def process_file(self, file): key = decrypt_key(base64_to_a32(key), self.master_key) if file['t'] == 0: k = file['k'] = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) - iv = file['iv'] = key[4:6] + (0, 0) - meta_mac = file['meta_mac'] = key[6:8] + file['iv'] = key[4:6] + (0, 0) + file['meta_mac'] = key[6:8] else: k = file['k'] = key attributes = base64_url_decode(file['a']) From b91f84cabd79af001853a3e396c52016c3e38ad5 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 7 Feb 2013 19:44:57 +0000 Subject: [PATCH 010/130] get_link() working, added get_file() for single file --- README.md | 13 ++++++++++--- mega.py | 39 +++++++++++++++++++++++---------------- tests/test.py | 18 ++++++++++-------- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 16a6d1d..d38123b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ This is a work in progress, further functionality coming shortly. files = m.get_files() +### Get single account file + + file = m.get_file('utYjgSTQ') + ### Upload a file m.upload('myfile.doc') @@ -30,10 +34,10 @@ This is a work in progress, further functionality coming shortly. m.download('utYjgSTQ','OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') -### Get a public link for a file - m.get_link('utYjgSTQ','OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') +### Get a public link for account file + m.get_link('utYjgSTQ') -### Search for a file +### Search account for a file m.find('myfile.doc') ### Trash a file from URL, it's ID, or from search @@ -63,3 +67,6 @@ This is a work in progress, further functionality coming shortly. - https://mega.co.nz/#developers + Thanks to http://juIien-marchand.fr/blog/contact for examples + + diff --git a/mega.py b/mega.py index b07474f..43d9ef6 100644 --- a/mega.py +++ b/mega.py @@ -91,11 +91,30 @@ def get_files(self): files_dict[file['h']] = self.process_file(file) return files_dict - def get_link(self, file_id, file_key): - if file_id and file_key: - return '{0}://{1}#!{2}!{3}'.format(self.schema, self.domain, file_id, file_key) + def get_file(self, file_id): + ''' + Return file object from given filename + ''' + files = self.get_files() + for file in files.items(): + if file[1]['h'] == file_id: + return file + + def get_link(self, file_id): + ''' + Get a files public link including decrypted key + ''' + file = self.get_file(file_id) + if file: + file_key = file[1]['k'] + if file_id and file_key: + public_handle = self.api_request({'a': 'l', 'n': file_id}) + decrypted_key = a32_to_base64(file_key) + return '{0}://{1}/#!%s!%s'.format(self.schema, self.domain) % (public_handle, decrypted_key) + else: + raise errors.ValidationError('File id and key must be set') else: - raise errors.RequestError('Url key missing') + raise errors.ValidationError('File not found') def download_url(self, url): path = self.parse_url(url).split('!') @@ -238,18 +257,6 @@ def download_file(self, file_id, file_key, is_public=False): if (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) != meta_mac: raise ValueError('Mismatched mac') - def get_public_url(self, file_id, file_key): - ''' - Get a files public link including decrypted key - ''' - if file_id and file_key: - public_handle = self.api_request({'a': 'l', 'n': file_id}) - decrypted_key = a32_to_base64(file_key) - return '{0}://{1}/#!%s!%s'.format(self.schema, self.domain) % (public_handle, decrypted_key) - else: - raise errors.ValidationError('File id and key must be set') - - def upload(self, filename, dest=None): #determine storage node if dest is None: diff --git a/tests/test.py b/tests/test.py index e549985..ec55448 100644 --- a/tests/test.py +++ b/tests/test.py @@ -21,6 +21,12 @@ def test(): if files[file]['a'] != False: print files[file] + ##get single file + #print(m.get_file('f14U0JhD')) + + ##get file's public link + #print(m.get_link('ChZCXTzA')) + ##upload file print(m.upload('test.py')) @@ -28,19 +34,15 @@ def test(): #print(m.delete('f14U0JhD')) #print(m.delete_url('https://mega.co.nz/#!f14U0JhD!S_2k-EvB5U1N3s0vm3I5C0JN2toHSGkVf0UxQsiKZ8A')) - ##search for a file on mega - files = m.find('test.py') - if files: + ##search for a file in account + file = m.find('test.py') + if file: #trash a file by it's id - #iterate to trash multiple results - print(m.delete(files[1]['k'])) + print(m.delete(file[1]['k'])) ##download file, by id+key or url #m.download('6hBW0R4a','By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') #m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') - ##get a public link for a file - print(m.get_link('6hBW0R4a','By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00')) - if __name__ == '__main__': test() \ No newline at end of file From a28031ca479deff8c70e751032cd57ce69e75e40 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 7 Feb 2013 19:53:22 +0000 Subject: [PATCH 011/130] updated features rundown --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d38123b..80e48a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Mega.py -Python library for the Mega.co.nz API, currently supporting login, uploading, downloading & deleting of files. +Python library for the Mega.co.nz API, currently supporting: + - login + - uploading + - downloading + - deleting + - searching + This is a work in progress, further functionality coming shortly. From 3cf4584be0c2af00c52a067dfa6ae7d4208e7a97 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 7 Feb 2013 23:30:37 +0000 Subject: [PATCH 012/130] get_link() partially working --- README.md | 14 ++++---------- mega.py | 30 ++++++++++-------------------- tests/test.py | 11 +++++------ 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 80e48a6..a0aa41a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Python library for the Mega.co.nz API, currently supporting: - downloading - deleting - searching - + This is a work in progress, further functionality coming shortly. @@ -28,21 +28,15 @@ This is a work in progress, further functionality coming shortly. files = m.get_files() -### Get single account file - - file = m.get_file('utYjgSTQ') +### Upload a file, and get its public link -### Upload a file - - m.upload('myfile.doc') + file = m.upload('myfile.doc') + m.get_link(file) ### Download a file from URL or it's ID,key combo m.download('utYjgSTQ','OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') -### Get a public link for account file - m.get_link('utYjgSTQ') - ### Search account for a file m.find('myfile.doc') diff --git a/mega.py b/mega.py index 43d9ef6..779fddd 100644 --- a/mega.py +++ b/mega.py @@ -9,6 +9,7 @@ import requests import errors from crypto import * +import ast class Mega(object): @@ -91,30 +92,19 @@ def get_files(self): files_dict[file['h']] = self.process_file(file) return files_dict - def get_file(self, file_id): - ''' - Return file object from given filename - ''' - files = self.get_files() - for file in files.items(): - if file[1]['h'] == file_id: - return file - - def get_link(self, file_id): + def get_link(self, file): ''' Get a files public link including decrypted key + Currently only works if passed upload() output ''' - file = self.get_file(file_id) - if file: - file_key = file[1]['k'] - if file_id and file_key: - public_handle = self.api_request({'a': 'l', 'n': file_id}) - decrypted_key = a32_to_base64(file_key) - return '{0}://{1}/#!%s!%s'.format(self.schema, self.domain) % (public_handle, decrypted_key) - else: - raise errors.ValidationError('File id and key must be set') + if 'f' in file: + file = file['f'][0] + public_handle = self.api_request({'a': 'l', 'n': file['h']}) + file_key = file['k'][file['k'].index(':') + 1:] + decrypted_key = a32_to_base64(decrypt_key(base64_to_a32(file_key), self.master_key)) + return '{0}://{1}/#!{2}!{3}'.format(self.schema, self.domain, public_handle, decrypted_key) else: - raise errors.ValidationError('File not found') + raise ValueError('This function requires upload() file object') def download_url(self, url): path = self.parse_url(url).split('!') diff --git a/tests/test.py b/tests/test.py index ec55448..d230e97 100644 --- a/tests/test.py +++ b/tests/test.py @@ -21,15 +21,14 @@ def test(): if files[file]['a'] != False: print files[file] - ##get single file - #print(m.get_file('f14U0JhD')) - - ##get file's public link - #print(m.get_link('ChZCXTzA')) - ##upload file print(m.upload('test.py')) + ##get file's public link + #NOTE: currently this only works with upload() file obj, as below + file = m.upload('test.py') + print(m.get_link(file)) + ##trash a file, by id or url #print(m.delete('f14U0JhD')) #print(m.delete_url('https://mega.co.nz/#!f14U0JhD!S_2k-EvB5U1N3s0vm3I5C0JN2toHSGkVf0UxQsiKZ8A')) From ec4b16048cda7e7ff9d00bf9f888d064d6954102 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 7 Feb 2013 23:38:05 +0000 Subject: [PATCH 013/130] get_link() renamed get_upload_link() --- README.md | 2 +- mega.py | 2 +- tests/test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a0aa41a..070a370 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This is a work in progress, further functionality coming shortly. ### Upload a file, and get its public link file = m.upload('myfile.doc') - m.get_link(file) + m.get_upload_link(file) ### Download a file from URL or it's ID,key combo m.download('utYjgSTQ','OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') diff --git a/mega.py b/mega.py index 779fddd..1b674ec 100644 --- a/mega.py +++ b/mega.py @@ -92,7 +92,7 @@ def get_files(self): files_dict[file['h']] = self.process_file(file) return files_dict - def get_link(self, file): + def get_upload_link(self, file): ''' Get a files public link including decrypted key Currently only works if passed upload() output diff --git a/tests/test.py b/tests/test.py index d230e97..01e8ef7 100644 --- a/tests/test.py +++ b/tests/test.py @@ -27,7 +27,7 @@ def test(): ##get file's public link #NOTE: currently this only works with upload() file obj, as below file = m.upload('test.py') - print(m.get_link(file)) + print(m.get_upload_link(file)) ##trash a file, by id or url #print(m.delete('f14U0JhD')) From e3fa3aba72983a9b88fc0ef6ce1f671a1c8ad7a7 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 7 Feb 2013 23:47:10 +0000 Subject: [PATCH 014/130] var renaming, comments --- mega.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/mega.py b/mega.py index 1b674ec..34c15fd 100644 --- a/mega.py +++ b/mega.py @@ -9,7 +9,6 @@ import requests import errors from crypto import * -import ast class Mega(object): @@ -94,8 +93,8 @@ def get_files(self): def get_upload_link(self, file): ''' - Get a files public link including decrypted key - Currently only works if passed upload() output + Get a files public link inc. decrypted key + Requires upload() response as input ''' if 'f' in file: file = file['f'][0] @@ -131,12 +130,12 @@ def get_user(self): def delete_url(self, url): #delete a file via it's url path = self.parse_url(url).split('!') - file_id = path[0] - return self.move(file_id, 4) + public_handle = path[0] + return self.move(public_handle, 4) - def delete(self, file_id): + def delete(self, public_handle): #straight delete by id - return self.move(file_id, 4) + return self.move(public_handle, 4) def find(self, filename): ''' @@ -147,7 +146,7 @@ def find(self, filename): if file[1]['a'] and file[1]['a']['n'] == filename: return file - def move(self, file_id, target): + def move(self, public_handle, target): #TODO node_id improvements ''' Move a file to another parent node @@ -163,7 +162,7 @@ def move(self, file_id, target): 4 : trash ''' #get node data - node_data = self.api_request({'a': 'f', 'f': 1, 'p': file_id}) + node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) target_node_id = str(self.get_node_by_type(target)[0]) node_id = None @@ -189,12 +188,12 @@ def get_node_by_type(self, type): return node - def download_file(self, file_id, file_key, is_public=False): + def download_file(self, file_handle, file_key, is_public=False): if is_public: file_key = base64_to_a32(file_key) - file_data = self.api_request({'a': 'g', 'g': 1, 'p': file_id}) + file_data = self.api_request({'a': 'g', 'g': 1, 'p': file_handle}) else: - file_data = self.api_request({'a': 'g', 'g': 1, 'n': file_id}) + 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], file_key[2] ^ file_key[6], file_key[3] ^ file_key[7]) From d8ab4d1d37e02b6a5743f8bbaa141aaf9065bf3e Mon Sep 17 00:00:00 2001 From: richardasaurus Date: Fri, 8 Feb 2013 12:21:32 +0000 Subject: [PATCH 015/130] Added get_link() and removed file['k'] from process_file() Added get_link() for generating public file download link, changes by jlejeune --- mega.py | 29 +++++++++++++++++++++++++---- tests/test.py | 13 +++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/mega.py b/mega.py index 34c15fd..c17e861 100644 --- a/mega.py +++ b/mega.py @@ -100,10 +100,31 @@ def get_upload_link(self, file): file = file['f'][0] public_handle = self.api_request({'a': 'l', 'n': file['h']}) file_key = file['k'][file['k'].index(':') + 1:] - decrypted_key = a32_to_base64(decrypt_key(base64_to_a32(file_key), self.master_key)) - return '{0}://{1}/#!{2}!{3}'.format(self.schema, self.domain, public_handle, decrypted_key) + decrypted_key = a32_to_base64(decrypt_key(base64_to_a32(file_key), + self.master_key)) + return '{0}://{1}/#!{2}!{3}'.format(self.schema, + self.domain, + public_handle, + decrypted_key) else: - raise ValueError('This function requires upload() file object') + raise ValueError('Upload() response required as input, use get_link() for regular file input') + + def get_link(self, file): + ''' + Get a file public link from given file object + ''' + file = file[1] + if 'h' in file and 'k' in file: + public_handle = self.api_request({'a': 'l', 'n': file['h']}) + file_key = file['k'][file['k'].index(':') + 1:] + decrypted_key = a32_to_base64(decrypt_key(base64_to_a32(file_key), + self.master_key)) + return '{0}://{1}/#!{2}!{3}'.format(self.schema, + self.domain, + public_handle, + decrypted_key) + else: + raise errors.ValidationError('File id and key must be present') def download_url(self, url): path = self.parse_url(url).split('!') @@ -316,7 +337,7 @@ def process_file(self, file): key = file['k'][file['k'].index(':') + 1:] key = decrypt_key(base64_to_a32(key), self.master_key) if file['t'] == 0: - k = file['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]) file['iv'] = key[4:6] + (0, 0) file['meta_mac'] = key[6:8] else: diff --git a/tests/test.py b/tests/test.py index 01e8ef7..d2308f5 100644 --- a/tests/test.py +++ b/tests/test.py @@ -2,8 +2,8 @@ def test(): #user details - email = 'your@email.com' - password = 'password' + email = 'richard@richard.do' + password = 'g3tmein1988' mega = Mega() @@ -25,16 +25,17 @@ def test(): print(m.upload('test.py')) ##get file's public link - #NOTE: currently this only works with upload() file obj, as below - file = m.upload('test.py') - print(m.get_upload_link(file)) + #NOTE: if passing upload() function response use get_upload_link() + file = m.find('test.py') + #print(m.get_upload_link(file)) + print(m.get_link(file)) ##trash a file, by id or url #print(m.delete('f14U0JhD')) #print(m.delete_url('https://mega.co.nz/#!f14U0JhD!S_2k-EvB5U1N3s0vm3I5C0JN2toHSGkVf0UxQsiKZ8A')) ##search for a file in account - file = m.find('test.py') + file = m.find('somefile.doc') if file: #trash a file by it's id print(m.delete(file[1]['k'])) From e1eed44e085481b4baa9851d9256f340afebf089 Mon Sep 17 00:00:00 2001 From: richardasaurus Date: Fri, 8 Feb 2013 12:23:28 +0000 Subject: [PATCH 016/130] Test updated --- tests/test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test.py b/tests/test.py index d2308f5..de15046 100644 --- a/tests/test.py +++ b/tests/test.py @@ -2,8 +2,8 @@ def test(): #user details - email = 'richard@richard.do' - password = 'g3tmein1988' + email = 'your@email.com' + password = 'password' mega = Mega() From 1d80921dc71745d26b81af040994e351a70cc798 Mon Sep 17 00:00:00 2001 From: richardasaurus Date: Fri, 8 Feb 2013 12:31:25 +0000 Subject: [PATCH 017/130] Added get_link() example --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 070a370..e6db386 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,9 @@ This is a work in progress, further functionality coming shortly. m.download('utYjgSTQ','OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') -### Search account for a file - m.find('myfile.doc') +### Search account for a file, and get its public link + file = m.find('myfile.doc') + m.get_link(file) ### Trash a file from URL, it's ID, or from search From 29ede8f6ed866121d91eed241f0b8bf630c08160 Mon Sep 17 00:00:00 2001 From: richardasaurus Date: Fri, 8 Feb 2013 12:45:16 +0000 Subject: [PATCH 018/130] download() now takes file obj, rather than public file id/handle --- README.md | 5 +++-- mega.py | 11 +++++++++-- tests/test.py | 3 ++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e6db386..e12ae3c 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,9 @@ This is a work in progress, further functionality coming shortly. file = m.upload('myfile.doc') m.get_upload_link(file) -### Download a file from URL or it's ID,key combo - m.download('utYjgSTQ','OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') +### Download a file from URL or file obj,key combo + file = m.find('myfile.doc') + m.download(file) m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') ### Search account for a file, and get its public link diff --git a/mega.py b/mega.py index c17e861..aa9e533 100644 --- a/mega.py +++ b/mega.py @@ -127,13 +127,20 @@ def get_link(self, file): raise errors.ValidationError('File id and key must be present') def download_url(self, url): + ''' + Download a file by it's public url + ''' path = self.parse_url(url).split('!') file_id = path[0] file_key = path[1] self.download_file(file_id, file_key, is_public=True) - def download(self, file_id, file_key): - self.download_file(file_id, file_key, is_public=True) + def download(self, file): + ''' + Download a file by it's file object + ''' + url = self.get_link(file) + self.download_url(url) def parse_url(self, url): #parse file id and key from url diff --git a/tests/test.py b/tests/test.py index de15046..507934b 100644 --- a/tests/test.py +++ b/tests/test.py @@ -41,7 +41,8 @@ def test(): print(m.delete(file[1]['k'])) ##download file, by id+key or url - #m.download('6hBW0R4a','By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') + #file = m.find('myfile.doc') + #m.download(file) #m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') if __name__ == '__main__': From 09268b1cb0568de257da31bb6111cd2ca914643e Mon Sep 17 00:00:00 2001 From: richardasaurus Date: Fri, 8 Feb 2013 12:48:49 +0000 Subject: [PATCH 019/130] features list --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e12ae3c..a93e591 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Python library for the Mega.co.nz API, currently supporting: - downloading - deleting - searching - + - sharing This is a work in progress, further functionality coming shortly. @@ -33,7 +33,7 @@ This is a work in progress, further functionality coming shortly. file = m.upload('myfile.doc') m.get_upload_link(file) -### Download a file from URL or file obj,key combo +### Download a file from URL or file obj file = m.find('myfile.doc') m.download(file) m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') From ff9152162821e36ccdc154c2397c969a36abeed5 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Fri, 8 Feb 2013 17:49:36 +0000 Subject: [PATCH 020/130] readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a93e591..fa24855 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,11 @@ This is a work in progress, further functionality coming shortly. Feel free to pull the source and make changes and additions. - Learn about the API at Mega.co.nz + Learn about the API at Mega.co.nz, more documentation coming shortly. - https://mega.co.nz/#developers - Thanks to http://juIien-marchand.fr/blog/contact for examples + +Thanks to http://julien-marchand.com/blog/contact for examples From 2493a10b8889f860c2caf0620e5dcf546ec632a7 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Sat, 9 Feb 2013 18:29:58 +0000 Subject: [PATCH 021/130] fixed api resp issue #2, reformatted code pep 8 style --- .gitignore | 7 ++++++ crypto.py | 15 +++++++++++-- errors.py | 2 +- mega.py | 62 +++++++++++++++++++++++++++++++-------------------- tests/test.py | 12 +++++----- 5 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48b9af1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Compiled source # +################### +*.pyc + +# Hidden files # +################### +.* \ No newline at end of file diff --git a/crypto.py b/crypto.py index 075ec1d..8a91356 100644 --- a/crypto.py +++ b/crypto.py @@ -5,20 +5,25 @@ import binascii import random + def aes_cbc_encrypt(data, key): aes_cipher = AES.new(key, AES.MODE_CBC, '\0' * 16) return aes_cipher.encrypt(data) + def aes_cbc_decrypt(data, key): aes_cipher = AES.new(key, AES.MODE_CBC, '\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] @@ -28,6 +33,7 @@ def stringhash(str, aeskey): 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): @@ -42,13 +48,13 @@ def prepare_key(arr): def encrypt_key(a, key): 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)), ()) def decrypt_key(a, key): 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)), ()) @@ -74,6 +80,7 @@ def str_to_a32(b): b += '\0' * (4 - len(b) % 4) return struct.unpack('>%dI' % (len(b) / 4), b) + def mpi_to_int(s): return int(binascii.hexlify(s[2:]), 16) @@ -84,18 +91,22 @@ def base64_url_decode(data): 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) 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): chunks = {} p = pp = 0 diff --git a/errors.py b/errors.py index e6d58c6..0b75334 100644 --- a/errors.py +++ b/errors.py @@ -1,10 +1,10 @@ - class ValidationError(Exception): """ Error in validation stage """ pass + class RequestError(Exception): """ Error in API request diff --git a/mega.py b/mega.py index aa9e533..c9ec679 100644 --- a/mega.py +++ b/mega.py @@ -10,12 +10,12 @@ import errors from crypto import * -class Mega(object): +class Mega(object): def __init__(self): self.schema = 'https' self.domain = 'mega.co.nz' - self.timeout = 160 #max time (secs) to wait for response from api requests + self.timeout = 160 #max time (secs) to wait for resp from api requests self.sid = None self.sequence_num = random.randint(0, 0xFFFFFFFF) self.request_id = make_id(10) @@ -46,13 +46,14 @@ def _login_process(self, resp, password): self.sid = resp['tsid'] elif 'csid' in resp: encrypted_rsa_private_key = base64_to_a32(resp['privk']) - rsa_private_key = decrypt_key(encrypted_rsa_private_key, self.master_key) + rsa_private_key = decrypt_key(encrypted_rsa_private_key, + self.master_key) private_key = a32_to_str(rsa_private_key) self.rsa_private_key = [0, 0, 0, 0] 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]) private_key = private_key[l:] @@ -73,8 +74,11 @@ def api_request(self, data): if self.sid: params.update({'sid': self.sid}) req = requests.post( - '{0}://g.api.{1}/cs'.format(self.schema,self.domain), params=params, data=json.dumps([data]), timeout=self.timeout) - json_resp = req.json() + '{0}://g.api.{1}/cs'.format(self.schema, self.domain), + params=params, + data=json.dumps([data]), + timeout=self.timeout) + json_resp = json.loads(req.text) #if numeric error code response if isinstance(json_resp, int): @@ -107,7 +111,8 @@ def get_upload_link(self, file): public_handle, decrypted_key) else: - raise ValueError('Upload() response required as input, use get_link() for regular file input') + raise ValueError('''Upload() response required as input, + use get_link() for regular file input''') def get_link(self, file): ''' @@ -144,7 +149,7 @@ def download(self, file): def parse_url(self, url): #parse file id and key from url - if('!' in url): + if ('!' in url): match = re.findall(r'/#!(.*)', url) path = match[0] return path @@ -152,7 +157,7 @@ def parse_url(self, url): raise errors.RequestError('Url key missing') def get_user(self): - user_data = self.api_request({'a': 'ug'}) + user_data = self.api_request({'a': 'ug'}) return user_data def delete_url(self, url): @@ -199,7 +204,8 @@ def move(self, public_handle, target): if i['h'] is not u'': node_id = i['h'] - return self.api_request({'a': 'm', 'n': node_id, 't': target_node_id, 'i': self.request_id}) + return self.api_request({'a': 'm', 'n': node_id, 't': target_node_id, + 'i': self.request_id}) def get_node_by_type(self, type): ''' @@ -212,7 +218,7 @@ def get_node_by_type(self, type): ''' nodes = self.get_files() for node in nodes.items(): - if(node[1]['t'] == type): + if (node[1]['t'] == type): return node @@ -234,7 +240,9 @@ def download_file(self, file_handle, file_key, is_public=False): attribs = decrypt_attr(attribs, k) file_name = attribs['n'] - print "downloading {0} (size: {1}), url = {2}".format(attribs['n'], file_size, file_url) + print "downloading {0} (size: {1}), url = {2}".format(attribs['n'], + file_size, + file_url) input_file = requests.get(file_url, stream=True).raw output_file = open(file_name, 'wb') @@ -251,7 +259,7 @@ def download_file(self, file_handle, file_key, is_public=False): chunk_mac = [iv[0], iv[1], iv[0], iv[1]] for i in range(0, len(chunk), 16): - block = chunk[i:i+16] + block = chunk[i:i + 16] if len(block) % 16: block += '\0' * (16 - (len(block) % 16)) block = str_to_a32(block) @@ -291,7 +299,7 @@ def upload(self, filename, dest=None): #generate random aes key (128) for file ul_key = [random.randint(0, 0xFFFFFFFF) for r in range(6)] - 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(a32_to_str(ul_key[:4]), AES.MODE_CTR, counter=count) file_mac = [0, 0, 0, 0] @@ -301,22 +309,25 @@ def upload(self, filename, dest=None): #determine chunks mac chunk_mac = [ul_key[4], ul_key[5], ul_key[4], ul_key[5]] for i in range(0, len(chunk), 16): - block = chunk[i:i+16] + block = chunk[i:i + 16] if len(block) % 16: block += '\0' * (16 - len(block) % 16) block = str_to_a32(block) - chunk_mac = [chunk_mac[0] ^ block[0], chunk_mac[1] ^ block[1], chunk_mac[2] ^ block[2], + chunk_mac = [chunk_mac[0] ^ block[0], chunk_mac[1] ^ block[1], + chunk_mac[2] ^ block[2], chunk_mac[3] ^ block[3]] chunk_mac = aes_cbc_encrypt_a32(chunk_mac, ul_key[:4]) #our files mac - file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1], file_mac[2] ^ chunk_mac[2], + file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1], + file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]] file_mac = aes_cbc_encrypt_a32(file_mac, ul_key[:4]) #encrypt file and upload chunk = aes.encrypt(chunk) - output_file = requests.post(ul_url + "/" + str(chunk_start), data=chunk, timeout=self.timeout) + output_file = requests.post(ul_url + "/" + str(chunk_start), + data=chunk, timeout=self.timeout) completion_file_handle = output_file.text #determine meta mac @@ -324,14 +335,16 @@ def upload(self, filename, dest=None): attribs = {'n': os.path.basename(filename)} encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4])) - key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], ul_key[2] ^ meta_mac[0], ul_key[3] ^ meta_mac[1], + key = [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[5], meta_mac[0], meta_mac[1]] encrypted_key = a32_to_base64(encrypt_key(key, self.master_key)) #update attributes - data = self.api_request({'a': 'p', 't': dest, 'n': [ - {'h': completion_file_handle, 't': 0, 'a': encrypt_attribs, 'k': encrypted_key}]} - ) - + data = self.api_request({'a': 'p', 't': dest, 'n': [{ + 'h': completion_file_handle, + 't': 0, + 'a': encrypt_attribs, + 'k': encrypted_key}]}) #close input file and return API msg input_file.close() return data @@ -344,7 +357,8 @@ def process_file(self, file): key = file['k'][file['k'].index(':') + 1:] key = decrypt_key(base64_to_a32(key), self.master_key) if file['t'] == 0: - 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]) file['iv'] = key[4:6] + (0, 0) file['meta_mac'] = key[6:8] else: diff --git a/tests/test.py b/tests/test.py index 507934b..a8cabc8 100644 --- a/tests/test.py +++ b/tests/test.py @@ -1,5 +1,6 @@ from mega import Mega + def test(): #user details email = 'your@email.com' @@ -8,7 +9,7 @@ def test(): mega = Mega() ##login - m = mega.login(email, password) + m = mega.login(email, password) ##get user details details = m.get_user() @@ -40,10 +41,11 @@ def test(): #trash a file by it's id print(m.delete(file[1]['k'])) - ##download file, by id+key or url - #file = m.find('myfile.doc') - #m.download(file) - #m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') + ##download file, by id+key or url + #file = m.find('myfile.doc') + #m.download(file) + #m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') + if __name__ == '__main__': test() \ No newline at end of file From 8dcd2d37eb039f20b46d8d13e073a9010a35c76a Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Mon, 11 Feb 2013 12:47:22 +0000 Subject: [PATCH 022/130] Added optional save destination path to download functions --- README.md | 3 ++- mega.py | 16 ++++++++++------ tests/test.py | 8 +++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fa24855..b726726 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,11 @@ This is a work in progress, further functionality coming shortly. file = m.upload('myfile.doc') m.get_upload_link(file) -### Download a file from URL or file obj +### Download a file from URL or file obj, optionally specify destination fold 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') ### Search account for a file, and get its public link file = m.find('myfile.doc') diff --git a/mega.py b/mega.py index c9ec679..46388b6 100644 --- a/mega.py +++ b/mega.py @@ -131,21 +131,21 @@ def get_link(self, file): else: raise errors.ValidationError('File id and key must be present') - def download_url(self, url): + def download_url(self, url, dest_path=None): ''' Download a file by it's public url ''' path = self.parse_url(url).split('!') file_id = path[0] file_key = path[1] - self.download_file(file_id, file_key, is_public=True) + self.download_file(file_id, file_key, dest_path, is_public=True) - def download(self, file): + def download(self, file, dest_path=None): ''' Download a file by it's file object ''' url = self.get_link(file) - self.download_url(url) + self.download_url(url, dest_path) def parse_url(self, url): #parse file id and key from url @@ -222,7 +222,7 @@ def get_node_by_type(self, type): return node - def download_file(self, file_handle, file_key, is_public=False): + def download_file(self, file_handle, file_key, dest_path=None, is_public=False): if is_public: file_key = base64_to_a32(file_key) file_data = self.api_request({'a': 'g', 'g': 1, 'p': file_handle}) @@ -245,7 +245,11 @@ def download_file(self, file_handle, file_key, is_public=False): file_url) input_file = requests.get(file_url, stream=True).raw - output_file = open(file_name, 'wb') + + if dest_path: + output_file = open(dest_path + '/' + file_name, 'wb') + else: + output_file = open(file_name, 'wb') counter = Counter.new( 128, initial_value=((iv[0] << 32) + iv[1]) << 64) diff --git a/tests/test.py b/tests/test.py index a8cabc8..7df00ba 100644 --- a/tests/test.py +++ b/tests/test.py @@ -41,9 +41,11 @@ def test(): #trash a file by it's id print(m.delete(file[1]['k'])) - ##download file, by id+key or url - #file = m.find('myfile.doc') - #m.download(file) + ##download file + #file = m.find('test.py') + #m.download(file) + ##specify destination folder + #m.download(file, '/home/user_name/Desktop') #m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') From e5ae47d1689ef783d7ee5a6e2fc2454d7c90342c Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 12 Feb 2013 19:21:44 +0000 Subject: [PATCH 023/130] Fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b726726..dea4620 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ This is a work in progress, further functionality coming shortly. file = m.upload('myfile.doc') m.get_upload_link(file) -### Download a file from URL or file obj, optionally specify destination fold +### Download a file from URL or file obj, optionally specify destination folder file = m.find('myfile.doc') m.download(file) m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') From d79dccace4f8a35b8f9c93bd17aa1dfc29adca98 Mon Sep 17 00:00:00 2001 From: Enrico Carlesso Date: Wed, 13 Feb 2013 15:08:09 +0100 Subject: [PATCH 024/130] Fixed utf-8 problem --- mega.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mega.py b/mega.py index 46388b6..f803679 100644 --- a/mega.py +++ b/mega.py @@ -240,7 +240,7 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False): attribs = decrypt_attr(attribs, k) file_name = attribs['n'] - print "downloading {0} (size: {1}), url = {2}".format(attribs['n'], + print "downloading {0} (size: {1}), url = {2}".format(attribs['n'].encode("utf8"), file_size, file_url) @@ -379,4 +379,4 @@ def process_file(self, file): elif file['t'] == 4: self.trashbin_id = file['h'] file['a'] = {'n': 'Rubbish Bin'} - return file \ No newline at end of file + return file From 17b4d0363af767f7686a11ba828270070995120e Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Sat, 16 Feb 2013 00:34:16 +0000 Subject: [PATCH 025/130] added setup.py, restructured files to be a python package. fixed issue #4. updated readme --- .gitignore | 5 ++++- CONTRIBUTORS | 2 +- README.md | 13 +++++++++++- mega/__init__.py | 1 + crypto.py => mega/crypto.py | 0 errors.py => mega/errors.py | 0 mega.py => mega/mega.py | 19 +++++++++++------ setup.py | 41 +++++++++++++++++++++++++++++++++++++ tests/test.py => tests.py | 7 +++---- 9 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 mega/__init__.py rename crypto.py => mega/crypto.py (100%) rename errors.py => mega/errors.py (100%) rename mega.py => mega/mega.py (96%) create mode 100644 setup.py rename tests/test.py => tests.py (92%) diff --git a/.gitignore b/.gitignore index 48b9af1..fcbedb6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ # Hidden files # ################### -.* \ No newline at end of file +.* +# Build files # +################### +build/ \ No newline at end of file diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9b87731..56116ee 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,4 +1,4 @@ cyberjujum richardasaurus jlejeune -issues: zbahoui,alyssarowan +issues: zbahoui,alyssarowan,kionez diff --git a/README.md b/README.md index dea4620..91b3437 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,17 @@ This is a work in progress, further functionality coming shortly. ## How To Use +### Install mega.py package + + - Download the source code + - Open the terminal in the source folder + - Run the following command + sudo python setup.py install + +### Import mega.py + + from mega import Mega + ### Create an instance of Mega.py mega = Mega() @@ -60,7 +71,7 @@ This is a work in progress, further functionality coming shortly. ## Tests - Test .py files can be found in /tests, run these to ensure Mega.py is working 100%. + Test .py files can be found in tests.py, run these to ensure Mega.py is working 100%. ## Contribute diff --git a/mega/__init__.py b/mega/__init__.py new file mode 100644 index 0000000..2da6f5f --- /dev/null +++ b/mega/__init__.py @@ -0,0 +1 @@ +from .mega import Mega \ No newline at end of file diff --git a/crypto.py b/mega/crypto.py similarity index 100% rename from crypto.py rename to mega/crypto.py diff --git a/errors.py b/mega/errors.py similarity index 100% rename from errors.py rename to mega/errors.py diff --git a/mega.py b/mega/mega.py similarity index 96% rename from mega.py rename to mega/mega.py index f803679..d46b434 100644 --- a/mega.py +++ b/mega/mega.py @@ -7,8 +7,8 @@ import random import binascii import requests -import errors -from crypto import * +from .errors import ValidationError,RequestError +from .crypto import * class Mega(object): @@ -32,7 +32,7 @@ def login_user(self, email, password): resp = self.api_request({'a': 'us', 'user': email, 'uh': uh}) #if numeric error code response if isinstance(resp, int): - raise errors.RequestError(resp) + raise RequestError(resp) self._login_process(resp, password_aes) def _login_process(self, resp, password): @@ -82,7 +82,7 @@ def api_request(self, data): #if numeric error code response if isinstance(json_resp, int): - raise errors.RequestError(json_resp) + raise RequestError(json_resp) return json_resp[0] def get_files(self): @@ -129,7 +129,7 @@ def get_link(self, file): public_handle, decrypted_key) else: - raise errors.ValidationError('File id and key must be present') + raise ValidationError('File id and key must be present') def download_url(self, url, dest_path=None): ''' @@ -154,7 +154,7 @@ def parse_url(self, url): path = match[0] return path else: - raise errors.RequestError('Url key missing') + raise RequestError('Url key missing') def get_user(self): user_data = self.api_request({'a': 'ug'}) @@ -359,6 +359,13 @@ def process_file(self, file): """ if file['t'] == 0 or file['t'] == 1: key = file['k'][file['k'].index(':') + 1:] + #fix for shared folder key format {k: foo1:bar1/foo2:bar2 } + uid = file['u'] + keys = file['k'].split('/') + regex = re.compile('^%s:.*$' % uid) + for keytmp in keys: + if regex.match(keytmp): + key = keytmp[keytmp.index(':') + 1:] key = decrypt_key(base64_to_a32(key), self.master_key) if file['t'] == 0: k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c658d28 --- /dev/null +++ b/setup.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -* + +from distutils.core import setup +import os + + +def get_packages(package): + """ + Return root package & all sub-packages. + """ + return [dirpath + for dirpath, dirnames, filenames in os.walk(package) + if os.path.exists(os.path.join(dirpath, '__init__.py'))] + +def get_package_data(package): + """ + Return all files under the root package, that are not in a + package themselves. + """ + walk = [(dirpath.replace(package + os.sep, '', 1), filenames) + for dirpath, dirnames, filenames in os.walk(package) + if not os.path.exists(os.path.join(dirpath, '__init__.py'))] + + filepaths = [] + for base, filenames in walk: + filepaths.extend([os.path.join(base, filename) + for filename in filenames]) + return {package: filepaths} + +setup( + name='mega.py', + version='0.8', + packages=get_packages('mega'), + package_data=get_package_data('mega'), + description='Python lib for the Mega.co.nz API', + author='Richard O\'Dwyer', + author_email='richard@richard.do', + license='Creative Commons Attribution-Noncommercial-Share Alike license', + long_description=open('README.md').read(), +) \ No newline at end of file diff --git a/tests/test.py b/tests.py similarity index 92% rename from tests/test.py rename to tests.py index 7df00ba..d8a4bb6 100644 --- a/tests/test.py +++ b/tests.py @@ -1,6 +1,5 @@ from mega import Mega - def test(): #user details email = 'your@email.com' @@ -23,11 +22,11 @@ def test(): print files[file] ##upload file - print(m.upload('test.py')) + print(m.upload('tests.py')) ##get file's public link #NOTE: if passing upload() function response use get_upload_link() - file = m.find('test.py') + file = m.find('tests.py') #print(m.get_upload_link(file)) print(m.get_link(file)) @@ -42,7 +41,7 @@ def test(): print(m.delete(file[1]['k'])) ##download file - #file = m.find('test.py') + #file = m.find('tests.py') #m.download(file) ##specify destination folder #m.download(file, '/home/user_name/Desktop') From 1dc6275c4e2ed9ce6fd130edec085f2ff831673a Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Sat, 16 Feb 2013 01:04:41 +0000 Subject: [PATCH 026/130] added pip install info --- .gitignore | 4 +++- README.md | 6 ++---- setup.py | 6 ++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index fcbedb6..188225a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ .* # Build files # ################### -build/ \ No newline at end of file +build/ +dist/ +.pypirc \ No newline at end of file diff --git a/README.md b/README.md index 91b3437..1bf24af 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,8 @@ This is a work in progress, further functionality coming shortly. ### Install mega.py package - - Download the source code - - Open the terminal in the source folder - - Run the following command - sudo python setup.py install + #Run the following command + sudo pip install mega.py ### Import mega.py diff --git a/setup.py b/setup.py index c658d28..e27df6d 100644 --- a/setup.py +++ b/setup.py @@ -38,4 +38,10 @@ def get_package_data(package): author_email='richard@richard.do', license='Creative Commons Attribution-Noncommercial-Share Alike license', long_description=open('README.md').read(), + classifiers=[ + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP' + ] ) \ No newline at end of file From 0cd6daae39efc650bb6a8e27ef38dc6f566ca263 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Sat, 16 Feb 2013 01:27:56 +0000 Subject: [PATCH 027/130] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1bf24af..d72384c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This is a work in progress, further functionality coming shortly. ### Install mega.py package - #Run the following command + #Run the following command, or run setup from the latest github source sudo pip install mega.py ### Import mega.py From 4128609e5dd0c1a1d1403b80c39d9c913bd13b9e Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Mon, 18 Feb 2013 22:35:57 +0000 Subject: [PATCH 028/130] get_files() updated to check each file has a name, thanks jlejeune --- mega/mega.py | 5 ++++- tests.py | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index d46b434..c21e432 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -92,7 +92,10 @@ def get_files(self): files = self.api_request({'a': 'f', 'c': 1}) files_dict = {} for file in files['f']: - files_dict[file['h']] = self.process_file(file) + processed_file = self.process_file(file) + #ensure each file has a name before returning + if processed_file['a']: + files_dict[file['h']] = processed_file return files_dict def get_upload_link(self, file): diff --git a/tests.py b/tests.py index d8a4bb6..e8f2491 100644 --- a/tests.py +++ b/tests.py @@ -18,8 +18,7 @@ def test(): files = m.get_files() #example iterate over files for file in files: - if files[file]['a'] != False: - print files[file] + print files[file] ##upload file print(m.upload('tests.py')) From 35c80f3c68ff00d72caade60de9c066d869d717e Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 19 Feb 2013 00:12:24 +0000 Subject: [PATCH 029/130] updated pip version info, added manifest to .gitignore --- .gitignore | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 188225a..1a4e3fd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ ################### build/ dist/ -.pypirc \ No newline at end of file +.pypirc +MANIFEST \ No newline at end of file diff --git a/setup.py b/setup.py index e27df6d..f3175fa 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.8', + version='0.8.1', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 27b636aa26d482e9beb02d56402988327bd355f7 Mon Sep 17 00:00:00 2001 From: richardasaurus Date: Fri, 22 Feb 2013 12:39:39 +0000 Subject: [PATCH 030/130] Added python-requests version 0.10+ requirement --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d72384c..4b790ba 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ This is a work in progress, further functionality coming shortly. ## Requirements 1. Python2.7+ - 2. Python requests - python-requests.org + 2. Python requests (>0.10) - python-requests.org 3. PyCrypto - dlitz.net/software/pycrypto/ ## Tests From c77ec993e90bc7a730a23a6e9b5b01e50890a978 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Sat, 23 Feb 2013 15:06:02 +0000 Subject: [PATCH 031/130] Added destory functions, empty trash, updated tests, other small refractoring --- README.md | 7 +- mega/mega.py | 228 ++++++++++++++++++++++++++++++++------------------- setup.py | 2 +- tests.py | 42 +++++----- 4 files changed, 171 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 4b790ba..268ec03 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,14 @@ This is a work in progress, further functionality coming shortly. file = m.find('myfile.doc') m.get_link(file) -### Trash a file from URL, it's ID, or from search +### Trash or destroy a file from URL, it's ID, or from search m.delete('utYjgSTQ') m.delete_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') + m.destroy('utYjgSTQ') + m.destroy_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') + files = m.find('myfile.doc') if files: m.delete(files[1]['k']) @@ -64,7 +67,7 @@ This is a work in progress, further functionality coming shortly. ## Requirements 1. Python2.7+ - 2. Python requests (>0.10) - python-requests.org + 2. Python requests (>0.10) - python-requests.org 3. PyCrypto - dlitz.net/software/pycrypto/ ## Tests diff --git a/mega/mega.py b/mega/mega.py index c21e432..e35a474 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -73,10 +73,15 @@ def api_request(self, data): if self.sid: params.update({'sid': self.sid}) + + #ensure input data is a list + if not isinstance(data, list): + data = [data] + req = requests.post( '{0}://g.api.{1}/cs'.format(self.schema, self.domain), params=params, - data=json.dumps([data]), + data=json.dumps(data), timeout=self.timeout) json_resp = json.loads(req.text) @@ -85,6 +90,61 @@ def api_request(self, data): raise RequestError(json_resp) return json_resp[0] + def parse_url(self, url): + #parse file id and key from url + if ('!' in url): + match = re.findall(r'/#!(.*)', url) + path = match[0] + return path + else: + raise RequestError('Url key missing') + + def process_file(self, file): + """ + Process a file... + """ + if file['t'] == 0 or file['t'] == 1: + key = file['k'][file['k'].index(':') + 1:] + #fix for shared folder key format {k: foo1:bar1/foo2:bar2 } + uid = file['u'] + keys = file['k'].split('/') + regex = re.compile('^%s:.*$' % uid) + for keytmp in keys: + if regex.match(keytmp): + key = keytmp[keytmp.index(':') + 1:] + key = decrypt_key(base64_to_a32(key), self.master_key) + if file['t'] == 0: + k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], + key[3] ^ key[7]) + file['iv'] = key[4:6] + (0, 0) + file['meta_mac'] = key[6:8] + else: + k = file['k'] = key + attributes = base64_url_decode(file['a']) + attributes = decrypt_attr(attributes, k) + file['a'] = attributes + elif file['t'] == 2: + self.root_id = file['h'] + file['a'] = {'n': 'Cloud Drive'} + elif file['t'] == 3: + self.inbox_id = file['h'] + file['a'] = {'n': 'Inbox'} + elif file['t'] == 4: + self.trashbin_id = file['h'] + file['a'] = {'n': 'Rubbish Bin'} + return file + + def find(self, filename): + ''' + Return file object from given filename + ''' + files = self.get_files() + for file in files.items(): + if file[1]['a'] and file[1]['a']['n'] == filename: + return file + + ########################################################################## + # GET def get_files(self): ''' Get all files in account @@ -134,34 +194,53 @@ def get_link(self, file): else: raise ValidationError('File id and key must be present') - def download_url(self, url, dest_path=None): + def get_user(self): + user_data = self.api_request({'a': 'ug'}) + return user_data + + def get_node_by_type(self, type): ''' - Download a file by it's public url + Get a node by it's numeric type id, e.g: + 0: file + 1: dir + 2: special: root cloud drive + 3: special: inbox + 4: special trash bin ''' - path = self.parse_url(url).split('!') - file_id = path[0] - file_key = path[1] - self.download_file(file_id, file_key, dest_path, is_public=True) + nodes = self.get_files() + for node in nodes.items(): + if (node[1]['t'] == type): + return node - def download(self, file, dest_path=None): + def get_files_in_node(self, target): ''' - Download a file by it's file object + Get all files in a given target, e.g. 4=trash ''' - url = self.get_link(file) - self.download_url(url, dest_path) + node_id = self.get_node_by_type(target) + files = self.api_request({'a': 'f', 'c': 1}) + files_dict = {} + for file in files['f']: + processed_file = self.process_file(file) + if processed_file['a'] and processed_file['p'] == node_id[0]: + files_dict[file['h']] = processed_file + return files_dict - def parse_url(self, url): - #parse file id and key from url - if ('!' in url): - match = re.findall(r'/#!(.*)', url) - path = match[0] - return path - else: - raise RequestError('Url key missing') + def get_id_from_public_handle(self, public_handle): + #get node data + node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) + node_id = None - def get_user(self): - user_data = self.api_request({'a': 'ug'}) - return user_data + #determine node id + for i in node_data['f']: + if i['h'] is not u'': + node_id = i['h'] + return node_id + + ########################################################################## + # DELETE + def delete(self, public_handle): + #straight delete by id + return self.move(public_handle, 4) def delete_url(self, url): #delete a file via it's url @@ -169,18 +248,17 @@ def delete_url(self, url): public_handle = path[0] return self.move(public_handle, 4) - def delete(self, public_handle): - #straight delete by id - return self.move(public_handle, 4) - - def find(self, filename): - ''' - Return file object from given filename - ''' - files = self.get_files() - for file in files.items(): - if file[1]['a'] and file[1]['a']['n'] == filename: - return file + def destroy(self, file_id): + #delete forever by private id + return self.api_request({'a': 'd', + 'n': file_id, + 'i': self.request_id}) + def destroy_url(self, url): + #delete a file via it's url + path = self.parse_url(url).split('!') + public_handle = path[0] + file_id = self.get_id_from_public_handle(public_handle) + return self.destroy(file_id) def move(self, public_handle, target): #TODO node_id improvements @@ -210,20 +288,37 @@ def move(self, public_handle, target): return self.api_request({'a': 'm', 'n': node_id, 't': target_node_id, 'i': self.request_id}) - def get_node_by_type(self, type): + + def empty_trash(self): + # get list of files in rubbish out + files = self.get_files_in_node(4) + + # make a list of json + if files != {}: + post_list = [] + for file in files: + post_list.append({"a": "d", + "n": file, + "i": self.request_id}) + return self.api_request(post_list) + + ########################################################################## + # DOWNLOAD + def download(self, file, dest_path=None): ''' - Get a node by it's numeric type id, e.g: - 0: file - 1: dir - 2: special: root cloud drive - 3: special: inbox - 4: special trash bin + Download a file by it's file object ''' - nodes = self.get_files() - for node in nodes.items(): - if (node[1]['t'] == type): - return node + url = self.get_link(file) + self.download_url(url, dest_path) + def download_url(self, url, dest_path=None): + ''' + Download a file by it's public url + ''' + path = self.parse_url(url).split('!') + file_id = path[0] + file_key = path[1] + self.download_file(file_id, file_key, dest_path, is_public=True) def download_file(self, file_handle, file_key, dest_path=None, is_public=False): if is_public: @@ -243,9 +338,9 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False): attribs = decrypt_attr(attribs, k) file_name = attribs['n'] - print "downloading {0} (size: {1}), url = {2}".format(attribs['n'].encode("utf8"), + print("downloading {0} (size: {1}), url = {2}".format(attribs['n'].encode("utf8"), file_size, - file_url) + file_url)) input_file = requests.get(file_url, stream=True).raw @@ -289,6 +384,8 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False): if (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) != meta_mac: raise ValueError('Mismatched mac') + ########################################################################## + # UPLOAD def upload(self, filename, dest=None): #determine storage node if dest is None: @@ -354,39 +451,4 @@ def upload(self, filename, dest=None): 'k': encrypted_key}]}) #close input file and return API msg input_file.close() - return data - - def process_file(self, file): - """ - Process a file... - """ - if file['t'] == 0 or file['t'] == 1: - key = file['k'][file['k'].index(':') + 1:] - #fix for shared folder key format {k: foo1:bar1/foo2:bar2 } - uid = file['u'] - keys = file['k'].split('/') - regex = re.compile('^%s:.*$' % uid) - for keytmp in keys: - if regex.match(keytmp): - key = keytmp[keytmp.index(':') + 1:] - key = decrypt_key(base64_to_a32(key), self.master_key) - if file['t'] == 0: - k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], - key[3] ^ key[7]) - file['iv'] = key[4:6] + (0, 0) - file['meta_mac'] = key[6:8] - else: - k = file['k'] = key - attributes = base64_url_decode(file['a']) - attributes = decrypt_attr(attributes, k) - file['a'] = attributes - elif file['t'] == 2: - self.root_id = file['h'] - file['a'] = {'n': 'Cloud Drive'} - elif file['t'] == 3: - self.inbox_id = file['h'] - file['a'] = {'n': 'Inbox'} - elif file['t'] == 4: - self.trashbin_id = file['h'] - file['a'] = {'n': 'Rubbish Bin'} - return file + return data \ No newline at end of file diff --git a/setup.py b/setup.py index f3175fa..1e56112 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.8.1', + version='0.8.2', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', diff --git a/tests.py b/tests.py index e8f2491..87397ec 100644 --- a/tests.py +++ b/tests.py @@ -7,45 +7,43 @@ def test(): mega = Mega() - ##login + #login m = mega.login(email, password) - ##get user details + #get user details details = m.get_user() print(details) - ##get account files + #get account files files = m.get_files() + #example iterate over files for file in files: print files[file] - ##upload file + #upload file print(m.upload('tests.py')) - ##get file's public link - #NOTE: if passing upload() function response use get_upload_link() + #search for a file in account file = m.find('tests.py') - #print(m.get_upload_link(file)) - print(m.get_link(file)) - - ##trash a file, by id or url - #print(m.delete('f14U0JhD')) - #print(m.delete_url('https://mega.co.nz/#!f14U0JhD!S_2k-EvB5U1N3s0vm3I5C0JN2toHSGkVf0UxQsiKZ8A')) - ##search for a file in account - file = m.find('somefile.doc') if file: - #trash a file by it's id - print(m.delete(file[1]['k'])) + #get public link + link = m.get_link(file) + print(link) - ##download file - #file = m.find('tests.py') - #m.download(file) - ##specify destination folder - #m.download(file, '/home/user_name/Desktop') - #m.download_url('https://mega.co.nz/#!6hBW0R4a!By7-Vjj5xal8K5w_IXH3PlGNyZ1VvIrjZkOmHGq1X00') + #download file. by file object or url + m.download(file, '/tmp') + #m.download_url(link) + + #delete or destroy file. by id or url + print(m.delete(file[1]['k'])) + #print(m.destroy(file[1]['h'])) + #print(m.delete_url(link)) + #print(m.destroy_url(link)) + #empty trash + print(m.empty_trash()) if __name__ == '__main__': test() \ No newline at end of file From 9b9530f453aed304861568f35e780fc22e3d2ec1 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Sat, 23 Feb 2013 15:08:36 +0000 Subject: [PATCH 032/130] print syntax --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 87397ec..036fc4e 100644 --- a/tests.py +++ b/tests.py @@ -19,7 +19,7 @@ def test(): #example iterate over files for file in files: - print files[file] + print(files[file]) #upload file print(m.upload('tests.py')) From 63d39dd8cd5eecee19d723ad2c6af30f09e4ac19 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Sat, 23 Feb 2013 15:22:12 +0000 Subject: [PATCH 033/130] moved find() --- mega/mega.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index e35a474..df4a8d4 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -134,6 +134,8 @@ def process_file(self, file): file['a'] = {'n': 'Rubbish Bin'} return file + ########################################################################## + # GET def find(self, filename): ''' Return file object from given filename @@ -143,8 +145,6 @@ def find(self, filename): if file[1]['a'] and file[1]['a']['n'] == filename: return file - ########################################################################## - # GET def get_files(self): ''' Get all files in account From fc8a03d649d5effab77cab7b8ec4d9bc2e934416 Mon Sep 17 00:00:00 2001 From: Julien LE JEUNE Date: Thu, 28 Feb 2013 08:50:57 +0100 Subject: [PATCH 034/130] Decrypt now shared folders/files that do not belong to us --- mega/mega.py | 71 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index df4a8d4..90b497b 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -101,28 +101,69 @@ def parse_url(self, url): def process_file(self, file): """ - Process a file... + Process a file """ if file['t'] == 0 or file['t'] == 1: - key = file['k'][file['k'].index(':') + 1:] - #fix for shared folder key format {k: foo1:bar1/foo2:bar2 } uid = file['u'] keys = file['k'].split('/') regex = re.compile('^%s:.*$' % uid) + key = None for keytmp in keys: if regex.match(keytmp): key = keytmp[keytmp.index(':') + 1:] - key = decrypt_key(base64_to_a32(key), self.master_key) - if file['t'] == 0: - k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], - key[3] ^ key[7]) - file['iv'] = key[4:6] + (0, 0) - file['meta_mac'] = key[6:8] - else: - k = file['k'] = key - attributes = base64_url_decode(file['a']) - attributes = decrypt_attr(attributes, k) - file['a'] = attributes + break + + # my objects + if key: + key = decrypt_key(base64_to_a32(key), self.master_key) + # file + if file['t'] == 0: + k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], + key[3] ^ key[7]) + file['iv'] = key[4:6] + (0, 0) + file['meta_mac'] = key[6:8] + # folder + else: + k = key + attributes = base64_url_decode(file['a']) + attributes = decrypt_attr(attributes, k) + file['a'] = attributes + # shared folders + elif 'su' in file and 'sk' in file and ':' in file['k']: + user_key = decrypt_key(base64_to_a32(file['sk']), + self.master_key) + key = decrypt_key(base64_to_a32(file['k'].split(':')[1]), + user_key) + # save user_key to decrypt shared files + self.users_keys[file['su']] = user_key + if file['t'] == 0: + k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], + key[3] ^ key[7]) + file['iv'] = key[4:6] + (0, 0) + file['meta_mac'] = key[6:8] + else: + k = key + attributes = base64_url_decode(file['a']) + attributes = decrypt_attr(attributes, k) + file['a'] = attributes + # shared files + elif file['u'] and ':' in file['k']: + user_key = self.users_keys[file['u']] + key = decrypt_key(base64_to_a32(file['k'].split(':')[1]), + user_key) + if file['t'] == 0: + k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], + key[3] ^ key[7]) + file['iv'] = key[4:6] + (0, 0) + file['meta_mac'] = key[6:8] + else: + k = key + attributes = base64_url_decode(file['a']) + attributes = decrypt_attr(attributes, k) + file['a'] = attributes + # other => wrong object + elif file['k'] == '': + file['a'] = False elif file['t'] == 2: self.root_id = file['h'] file['a'] = {'n': 'Cloud Drive'} @@ -451,4 +492,4 @@ def upload(self, filename, dest=None): 'k': encrypted_key}]}) #close input file and return API msg input_file.close() - return data \ No newline at end of file + return data From 6ecbc2c3d56f55e0834a5ae3f21f21a38cf3b175 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Thu, 28 Feb 2013 12:19:15 +0000 Subject: [PATCH 035/130] updated version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1e56112..a6a0455 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.8.2', + version='0.8.3', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 46e5328b4c411b27ff90a5503d7e3b27fae3e8e0 Mon Sep 17 00:00:00 2001 From: Julien LE JEUNE Date: Thu, 28 Feb 2013 17:26:34 +0100 Subject: [PATCH 036/130] Forget to init self.users_keys variable --- mega/mega.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mega/mega.py b/mega/mega.py index 90b497b..47bd73a 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -38,6 +38,7 @@ def login_user(self, email, password): def _login_process(self, resp, password): encrypted_master_key = base64_to_a32(resp['k']) self.master_key = decrypt_key(encrypted_master_key, password) + self.users_keys = dict() if 'tsid' in resp: tsid = base64_url_decode(resp['tsid']) key_encrypted = a32_to_str( From d444be92b401b4cf2369980489e2686820c3b606 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 5 Mar 2013 01:48:01 +0000 Subject: [PATCH 037/130] changes to delete, destroy functions. see readme for updated usage. delete_url functions non-working currently --- README.md | 8 ++++---- mega/mega.py | 22 +++++++++------------- tests.py | 4 ++-- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 268ec03..44fe816 100644 --- a/README.md +++ b/README.md @@ -52,17 +52,17 @@ This is a work in progress, further functionality coming shortly. file = m.find('myfile.doc') m.get_link(file) -### Trash or destroy a file from URL, it's ID, or from search +### Trash or destroy a file from URL or its ID - m.delete('utYjgSTQ') + m.delete(file[0]) m.delete_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') - m.destroy('utYjgSTQ') + m.destroy(file[0]) m.destroy_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') files = m.find('myfile.doc') if files: - m.delete(files[1]['k']) + m.delete(files[0]) ## Requirements diff --git a/mega/mega.py b/mega/mega.py index 47bd73a..0a6b4fe 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -269,6 +269,7 @@ def get_files_in_node(self, target): def get_id_from_public_handle(self, public_handle): #get node data + #TODO fix this function node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) node_id = None @@ -288,7 +289,8 @@ def delete_url(self, url): #delete a file via it's url path = self.parse_url(url).split('!') public_handle = path[0] - return self.move(public_handle, 4) + file_id = self.get_id_from_public_handle(public_handle) + return self.move(file_id, 4) def destroy(self, file_id): #delete forever by private id @@ -302,8 +304,7 @@ def destroy_url(self, url): file_id = self.get_id_from_public_handle(public_handle) return self.destroy(file_id) - def move(self, public_handle, target): - #TODO node_id improvements + def move(self, file_id, target): ''' Move a file to another parent node params: @@ -317,17 +318,12 @@ def move(self, public_handle, target): 3 : inbox 4 : trash ''' - #get node data - node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) - target_node_id = str(self.get_node_by_type(target)[0]) - node_id = None - #determine node id - for i in node_data['f']: - if i['h'] is not u'': - node_id = i['h'] - - return self.api_request({'a': 'm', 'n': node_id, 't': target_node_id, + #determine target_node_id + target_node_id = str(self.get_node_by_type(target)[0]) + return self.api_request({'a': 'm', + 'n': file_id, + 't': target_node_id, 'i': self.request_id}) diff --git a/tests.py b/tests.py index 036fc4e..f7d5906 100644 --- a/tests.py +++ b/tests.py @@ -37,8 +37,8 @@ def test(): #m.download_url(link) #delete or destroy file. by id or url - print(m.delete(file[1]['k'])) - #print(m.destroy(file[1]['h'])) + print(m.delete(file[0])) + #print(m.destroy(file[0])) #print(m.delete_url(link)) #print(m.destroy_url(link)) From 9096e4cf7f4286a3224206907e11a207c8054213 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 5 Mar 2013 01:52:37 +0000 Subject: [PATCH 038/130] Added syntax to readme --- README.md | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 44fe816..d70e9db 100644 --- a/README.md +++ b/README.md @@ -13,47 +13,49 @@ This is a work in progress, further functionality coming shortly. ## How To Use ### Install mega.py package - +'''python #Run the following command, or run setup from the latest github source sudo 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) - +''' ### Get user details - +'''python details = m.get_user() - +''' ### 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) - +''' ### 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') - +''' ### Search account for a file, and get its public link +'''python file = m.find('myfile.doc') m.get_link(file) - +''' ### Trash or destroy a file from URL or its ID - +'''python m.delete(file[0]) m.delete_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') @@ -63,7 +65,7 @@ This is a work in progress, further functionality coming shortly. files = m.find('myfile.doc') if files: m.delete(files[0]) - +''' ## Requirements 1. Python2.7+ From a7c8089cad8c2fe03a4b49f8f9a40423f50efb93 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 5 Mar 2013 01:54:04 +0000 Subject: [PATCH 039/130] Added syntax to readme --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d70e9db..080ac65 100644 --- a/README.md +++ b/README.md @@ -13,49 +13,49 @@ This is a work in progress, further functionality coming shortly. ## How To Use ### Install mega.py package -'''python +```python #Run the following command, or run setup from the latest github source sudo pip install mega.py -''' +``` ### Import mega.py -'''python +```python from mega import Mega -''' +``` ### Create an instance of Mega.py -'''python +```python mega = Mega() -''' +``` ### Login to Mega -'''python +```python m = mega.login(email, password) -''' +``` ### Get user details -'''python +```python details = m.get_user() -''' +``` ### Get account files -'''python +```python files = m.get_files() -''' +``` ### Upload a file, and get its public link -'''python +```python file = m.upload('myfile.doc') m.get_upload_link(file) -''' +``` ### Download a file from URL or file obj, optionally specify destination folder -'''python +```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') -''' +``` ### Search account for a file, and get its public link -'''python +```python file = m.find('myfile.doc') m.get_link(file) -''' +``` ### Trash or destroy a file from URL or its ID -'''python +```python m.delete(file[0]) m.delete_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') @@ -65,7 +65,7 @@ This is a work in progress, further functionality coming shortly. files = m.find('myfile.doc') if files: m.delete(files[0]) -''' +``` ## Requirements 1. Python2.7+ From b6ae0584ff466ef972c9bb58bd7b2c4039985373 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 5 Mar 2013 01:55:25 +0000 Subject: [PATCH 040/130] Spacing --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 080ac65..ab1cc81 100644 --- a/README.md +++ b/README.md @@ -14,24 +14,24 @@ This is a work in progress, further functionality coming shortly. ### Install mega.py package ```python - #Run the following command, or run setup from the latest github source - sudo pip install mega.py +#Run the following command, or run setup from the latest github source +sudo pip install mega.py ``` ### Import mega.py ```python - from mega import Mega +from mega import Mega ``` ### Create an instance of Mega.py ```python - mega = Mega() +mega = Mega() ``` ### Login to Mega ```python - m = mega.login(email, password) +m = mega.login(email, password) ``` ### Get user details ```python - details = m.get_user() +details = m.get_user() ``` ### Get account files ```python @@ -39,32 +39,32 @@ This is a work in progress, further functionality coming shortly. ``` ### Upload a file, and get its public link ```python - file = m.upload('myfile.doc') - m.get_upload_link(file) +file = m.upload('myfile.doc') +m.get_upload_link(file) ``` ### 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') +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') ``` ### Search account for a file, and get its public link ```python - file = m.find('myfile.doc') - m.get_link(file) +file = m.find('myfile.doc') +m.get_link(file) ``` ### Trash or destroy a file from URL or its ID ```python - m.delete(file[0]) - m.delete_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') +m.delete(file[0]) +m.delete_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') - m.destroy(file[0]) - m.destroy_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') +m.destroy(file[0]) +m.destroy_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') - files = m.find('myfile.doc') - if files: - m.delete(files[0]) +files = m.find('myfile.doc') +if files: + m.delete(files[0]) ``` ## Requirements From 43296ee1be8e8b0759d516b3ceb28d792e5d01e3 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 5 Mar 2013 01:56:02 +0000 Subject: [PATCH 041/130] Spacing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab1cc81..40eea0a 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ details = m.get_user() ``` ### Get account files ```python - files = m.get_files() +files = m.get_files() ``` ### Upload a file, and get its public link ```python From 8f064350fde386e38a0f048481ae64cbac1fdc83 Mon Sep 17 00:00:00 2001 From: "richard@richard.do" Date: Tue, 5 Mar 2013 01:58:30 +0000 Subject: [PATCH 042/130] Version number for pypi --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a6a0455..3a4eaca 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.8.3', + version='0.8.5', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 83b1de1662c50004c3ae89bcb73cfdd71a53c0c8 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 10 Mar 2013 03:38:22 +0000 Subject: [PATCH 043/130] added get_quota() and get_balance() --- mega/mega.py | 16 ++++++++++++++++ tests.py | 8 ++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 0a6b4fe..a116be4 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -279,6 +279,22 @@ def get_id_from_public_handle(self, public_handle): node_id = i['h'] return node_id + def get_quota(self): + ''' + Get current remaining disk quota in MegaBytes + ''' + json_resp = self.api_request({'a': 'uq', 'xfer': 1}) + #convert bytes to megabyes + return json_resp['mstrg']/1048576 + + def get_balance(self): + ''' + Get account monetary balance, Pro accounts only + ''' + user_data = self.api_request({"a":"uq","pro":1}) + if 'balance' in user_data: + return user_data['balance'] + ########################################################################## # DELETE def delete(self, public_handle): diff --git a/tests.py b/tests.py index f7d5906..fdc71e9 100644 --- a/tests.py +++ b/tests.py @@ -2,8 +2,8 @@ def test(): #user details - email = 'your@email.com' - password = 'password' + email = 'richard@richard.do' + password = 'fofput2' mega = Mega() @@ -17,6 +17,9 @@ def test(): #get account files files = m.get_files() + #get account disk quota in MB + print(m.get_quota()) + #example iterate over files for file in files: print(files[file]) @@ -27,6 +30,7 @@ def test(): #search for a file in account file = m.find('tests.py') + if file: #get public link link = m.get_link(file) From f84d5bf3114b60025728a58a52af7385eaaafdf2 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 10 Mar 2013 03:39:06 +0000 Subject: [PATCH 044/130] added get_quota() and get_balance() --- mega/mega.py | 2 +- tests.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index a116be4..7f877dc 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -273,7 +273,7 @@ def get_id_from_public_handle(self, public_handle): node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) node_id = None - #determine node id + #determine node id for i in node_data['f']: if i['h'] is not u'': node_id = i['h'] diff --git a/tests.py b/tests.py index fdc71e9..1bf257e 100644 --- a/tests.py +++ b/tests.py @@ -2,8 +2,8 @@ def test(): #user details - email = 'richard@richard.do' - password = 'fofput2' + email = 'your@email.com' + password = 'password' mega = Mega() From 3743298f66ad6049c20bf21cecb93f24e4c81f2a Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 10 Mar 2013 03:53:24 +0000 Subject: [PATCH 045/130] updated readme --- README.md | 8 ++++++++ mega/mega.py | 2 +- setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40eea0a..64315c6 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,14 @@ m = mega.login(email, password) ```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 files ```python files = m.get_files() diff --git a/mega/mega.py b/mega/mega.py index 7f877dc..a116be4 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -273,7 +273,7 @@ def get_id_from_public_handle(self, public_handle): node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) node_id = None - #determine node id + #determine node id for i in node_data['f']: if i['h'] is not u'': node_id = i['h'] diff --git a/setup.py b/setup.py index 3a4eaca..c57bc49 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.8.5', + version='0.8.6', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From b43e636d79738709b87b444c943ae71ce6ef4e79 Mon Sep 17 00:00:00 2001 From: Julien LE JEUNE Date: Tue, 12 Mar 2013 15:40:56 +0100 Subject: [PATCH 046/130] Add create_folder() function --- mega/mega.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/mega/mega.py b/mega/mega.py index a116be4..d69f9ad 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -506,3 +506,36 @@ def upload(self, filename, dest=None): #close input file and return API msg input_file.close() return data + + def create_folder(self, name, dest=None): + #determine storage node + if dest is None: + #if none set, upload to cloud drive node + if not hasattr(self, 'root_id'): + self.get_files() + dest = self.root_id + + #generate random aes key (128) for folder + ul_key = [random.randint(0, 0xFFFFFFFF) for r in range(6)] + + #encrypt attribs + attribs = {'n': name} + encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4])) + encrypted_key = a32_to_base64(encrypt_key(ul_key[:4], self.master_key)) + + #update attributes + data = self.api_request({'a': 'p', + 't': dest, + 'n': [{ + 'h': 'xxxxxxxx', + 't': 1, + 'a': encrypt_attribs, + 'k': encrypted_key} + ]}) + + #update files_dict + del self.files_dict + self.get_files() + + #return API msg + return data From 7b1e471c9123133845c239f74762c63feb394554 Mon Sep 17 00:00:00 2001 From: Julien LE JEUNE Date: Tue, 12 Mar 2013 15:47:25 +0100 Subject: [PATCH 047/130] Send also request_id param in create_folder() function (useless but to respect API) --- mega/mega.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mega/mega.py b/mega/mega.py index d69f9ad..e201cb3 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -531,7 +531,8 @@ def create_folder(self, name, dest=None): 't': 1, 'a': encrypt_attribs, 'k': encrypted_key} - ]}) + ], + 'i': self.request_id}) #update files_dict del self.files_dict From 63b5a224de2edc999419be29a8dbca6c06c8723c Mon Sep 17 00:00:00 2001 From: richard Date: Tue, 12 Mar 2013 16:51:54 +0000 Subject: [PATCH 048/130] remove filesdict update --- mega/mega.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index e201cb3..f6b094b 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -533,10 +533,5 @@ def create_folder(self, name, dest=None): 'k': encrypted_key} ], 'i': self.request_id}) - - #update files_dict - del self.files_dict - self.get_files() - #return API msg return data From 0592055bdc437e6d444a288e311e3a27a5357b3c Mon Sep 17 00:00:00 2001 From: richard Date: Tue, 12 Mar 2013 16:56:17 +0000 Subject: [PATCH 049/130] updated readme, create_folder() e.g --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 64315c6..5a2eb87 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,10 @@ m.download(file) m.download_url('https://mega.co.nz/#!utYjgSTQ!OM4U3V5v_W4N5edSo0wolg1D5H0fwSrLD3oLnLuS9pc') m.download(file, '/home/john-smith/Desktop') ``` +### Create a folder +```python +m.create_folder('new_folder') +``` ### Search account for a file, and get its public link ```python file = m.find('myfile.doc') @@ -93,6 +97,3 @@ if files: -Thanks to http://julien-marchand.com/blog/contact for examples - - From 38db6760d5b545f616fee993d5953ba9786d5f67 Mon Sep 17 00:00:00 2001 From: richard Date: Tue, 12 Mar 2013 16:56:57 +0000 Subject: [PATCH 050/130] updated version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c57bc49..4101960 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.8.6', + version='0.9', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From be7fa68da8824d877c5ca7e3ebff670b8d37081a Mon Sep 17 00:00:00 2001 From: richard Date: Tue, 12 Mar 2013 17:00:30 +0000 Subject: [PATCH 051/130] upload() minor tidy up --- mega/mega.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index f6b094b..0183fff 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -444,9 +444,7 @@ def upload(self, filename, dest=None): #determine storage node if dest is None: #if none set, upload to cloud drive node - if hasattr(self, 'root_id'): - root_id = getattr(self, 'root_id') - else: + if not hasattr(self, 'root_id'): self.get_files() dest = self.root_id From fd0518620bff1e0821f604d646c2ce6fa1d09907 Mon Sep 17 00:00:00 2001 From: richard Date: Mon, 8 Apr 2013 14:18:38 +0100 Subject: [PATCH 052/130] pip install error fixed --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 4101960..c3c3334 100644 --- a/setup.py +++ b/setup.py @@ -30,14 +30,14 @@ def get_package_data(package): setup( name='mega.py', - version='0.9', + version='0.9.2', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', author='Richard O\'Dwyer', author_email='richard@richard.do', license='Creative Commons Attribution-Noncommercial-Share Alike license', - long_description=open('README.md').read(), + long_description='https://github.com/richardasaurus/mega.py', classifiers=[ 'Intended Audience :: Developers', 'Operating System :: OS Independent', From 692e774cd710e50b1f87bc846d8f4b69dbf79546 Mon Sep 17 00:00:00 2001 From: gissehel Date: Mon, 15 Apr 2013 16:04:03 +0200 Subject: [PATCH 053/130] Shared keys now works for file comming from shared folders that aren't shared anymore. --- mega/mega.py | 90 +++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 0183fff..dbae677 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -38,7 +38,6 @@ def login_user(self, email, password): def _login_process(self, resp, password): encrypted_master_key = base64_to_a32(resp['k']) self.master_key = decrypt_key(encrypted_master_key, password) - self.users_keys = dict() if 'tsid' in resp: tsid = base64_url_decode(resp['tsid']) key_encrypted = a32_to_str( @@ -100,63 +99,40 @@ def parse_url(self, url): else: raise RequestError('Url key missing') - def process_file(self, file): + def process_file(self, file, shared_keys): """ Process a file """ if file['t'] == 0 or file['t'] == 1: + keys = dict(keypart.split(':',1) for keypart in file['k'].split('/')) uid = file['u'] - keys = file['k'].split('/') - regex = re.compile('^%s:.*$' % uid) key = None - for keytmp in keys: - if regex.match(keytmp): - key = keytmp[keytmp.index(':') + 1:] - break - # my objects - if key: - key = decrypt_key(base64_to_a32(key), self.master_key) - # file - if file['t'] == 0: - k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], - key[3] ^ key[7]) - file['iv'] = key[4:6] + (0, 0) - file['meta_mac'] = key[6:8] - # folder - else: - k = key - attributes = base64_url_decode(file['a']) - attributes = decrypt_attr(attributes, k) - file['a'] = attributes - # shared folders + if uid in keys : + key = decrypt_key(base64_to_a32( keys[uid] ), self.master_key) + # shared folders elif 'su' in file and 'sk' in file and ':' in file['k']: - user_key = decrypt_key(base64_to_a32(file['sk']), - self.master_key) - key = decrypt_key(base64_to_a32(file['k'].split(':')[1]), - user_key) - # save user_key to decrypt shared files - self.users_keys[file['su']] = user_key - if file['t'] == 0: - k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], - key[3] ^ key[7]) - file['iv'] = key[4:6] + (0, 0) - file['meta_mac'] = key[6:8] - else: - k = key - attributes = base64_url_decode(file['a']) - attributes = decrypt_attr(attributes, k) - file['a'] = attributes + shared_key = decrypt_key(base64_to_a32(file['sk']),self.master_key) + key = decrypt_key(base64_to_a32(keys[file['h']]),shared_key) + if file['su'] not in shared_keys : + shared_keys[file['su']] = {} + shared_keys[file['su']][file['h']] = shared_key # shared files - elif file['u'] and ':' in file['k']: - user_key = self.users_keys[file['u']] - key = decrypt_key(base64_to_a32(file['k'].split(':')[1]), - user_key) + elif file['u'] and file['u'] in shared_keys : + for hkey in shared_keys[file['u']] : + shared_key = shared_keys[file['u']][hkey] + if hkey in keys : + key = keys[hkey] + key = decrypt_key(base64_to_a32(key),shared_key) + break + if key is not None : + # file if file['t'] == 0: k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) file['iv'] = key[4:6] + (0, 0) file['meta_mac'] = key[6:8] + # folder else: k = key attributes = base64_url_decode(file['a']) @@ -176,6 +152,24 @@ def process_file(self, file): file['a'] = {'n': 'Rubbish Bin'} return file + def init_shared_keys(self,files,shared_keys) : + ''' + Init shared key not associated with a user. + It seems to happen when a folder is shared, + some files are exachanged and then the + folder is not shared anymore. + Keys are stored in files['s'] and files['ok'] + ''' + ok_dict = {} + for ok_item in files['ok'] : + shared_key = decrypt_key(base64_to_a32(ok_item['k']),self.master_key) + ok_dict[ok_item['h']] = shared_key + for s_item in files['s'] : + if s_item['u'] not in shared_keys : + shared_keys[s_item['u']] = {} + if s_item['h'] in ok_dict : + shared_keys[s_item['u']][s_item['h']] = ok_dict[s_item['h']] + ########################################################################## # GET def find(self, filename): @@ -193,8 +187,10 @@ def get_files(self): ''' files = self.api_request({'a': 'f', 'c': 1}) files_dict = {} + shared_keys = {} + self.init_shared_keys(files,shared_keys) for file in files['f']: - processed_file = self.process_file(file) + processed_file = self.process_file(file,shared_keys) #ensure each file has a name before returning if processed_file['a']: files_dict[file['h']] = processed_file @@ -261,8 +257,10 @@ def get_files_in_node(self, target): node_id = self.get_node_by_type(target) files = self.api_request({'a': 'f', 'c': 1}) files_dict = {} + shared_keys = {} + self.init_shared_keys(files,shared_keys) for file in files['f']: - processed_file = self.process_file(file) + processed_file = self.process_file(file,shared_keys) if processed_file['a'] and processed_file['p'] == node_id[0]: files_dict[file['h']] = processed_file return files_dict From 8b1052a214772bb57a0ab18e5fc351a3ff200ddf Mon Sep 17 00:00:00 2001 From: richard Date: Mon, 15 Apr 2013 16:14:55 +0100 Subject: [PATCH 054/130] Code to pep8 standards, shared folders working --- mega/mega.py | 124 +++++++++++++++++++++++++-------------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index dbae677..f1be9eb 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -7,7 +7,7 @@ import random import binascii import requests -from .errors import ValidationError,RequestError +from .errors import ValidationError, RequestError from .crypto import * @@ -15,14 +15,14 @@ class Mega(object): def __init__(self): self.schema = 'https' self.domain = 'mega.co.nz' - self.timeout = 160 #max time (secs) to wait for resp from api requests + self.timeout = 160 # max time (secs) to wait for resp from api requests self.sid = None self.sequence_num = random.randint(0, 0xFFFFFFFF) self.request_id = make_id(10) @classmethod - def login(class_, email, password): - instance = class_() + def login(cls, email, password): + instance = cls() instance.login_user(email, password) return instance @@ -53,7 +53,7 @@ def _login_process(self, resp, password): self.rsa_private_key = [0, 0, 0, 0] 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]) private_key = private_key[l:] @@ -80,9 +80,9 @@ def api_request(self, data): req = requests.post( '{0}://g.api.{1}/cs'.format(self.schema, self.domain), - params=params, - data=json.dumps(data), - timeout=self.timeout) + params=params, + data=json.dumps(data), + timeout=self.timeout) json_resp = json.loads(req.text) #if numeric error code response @@ -104,28 +104,28 @@ def process_file(self, file, shared_keys): Process a file """ if file['t'] == 0 or file['t'] == 1: - keys = dict(keypart.split(':',1) for keypart in file['k'].split('/')) + keys = dict(keypart.split(':', 1) for keypart in file['k'].split('/')) uid = file['u'] key = None # my objects - if uid in keys : - key = decrypt_key(base64_to_a32( keys[uid] ), self.master_key) + if uid in keys: + key = decrypt_key(base64_to_a32(keys[uid]), self.master_key) # shared folders elif 'su' in file and 'sk' in file and ':' in file['k']: - shared_key = decrypt_key(base64_to_a32(file['sk']),self.master_key) - key = decrypt_key(base64_to_a32(keys[file['h']]),shared_key) - if file['su'] not in shared_keys : + shared_key = decrypt_key(base64_to_a32(file['sk']), self.master_key) + key = decrypt_key(base64_to_a32(keys[file['h']]), shared_key) + if file['su'] not in shared_keys: shared_keys[file['su']] = {} shared_keys[file['su']][file['h']] = shared_key # shared files - elif file['u'] and file['u'] in shared_keys : - for hkey in shared_keys[file['u']] : + elif file['u'] and file['u'] in shared_keys: + for hkey in shared_keys[file['u']]: shared_key = shared_keys[file['u']][hkey] - if hkey in keys : + if hkey in keys: key = keys[hkey] - key = decrypt_key(base64_to_a32(key),shared_key) + key = decrypt_key(base64_to_a32(key), shared_key) break - if key is not None : + if key is not None: # file if file['t'] == 0: k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], @@ -152,55 +152,55 @@ def process_file(self, file, shared_keys): file['a'] = {'n': 'Rubbish Bin'} return file - def init_shared_keys(self,files,shared_keys) : - ''' + def init_shared_keys(self, files, shared_keys): + """ Init shared key not associated with a user. - It seems to happen when a folder is shared, - some files are exachanged and then the - folder is not shared anymore. + Seems to happen when a folder is shared, + some files are exchanged and then the + folder is un-shared. Keys are stored in files['s'] and files['ok'] - ''' + """ ok_dict = {} - for ok_item in files['ok'] : - shared_key = decrypt_key(base64_to_a32(ok_item['k']),self.master_key) + for ok_item in files['ok']: + shared_key = decrypt_key(base64_to_a32(ok_item['k']), self.master_key) ok_dict[ok_item['h']] = shared_key - for s_item in files['s'] : - if s_item['u'] not in shared_keys : + for s_item in files['s']: + if s_item['u'] not in shared_keys: shared_keys[s_item['u']] = {} - if s_item['h'] in ok_dict : + if s_item['h'] in ok_dict: shared_keys[s_item['u']][s_item['h']] = ok_dict[s_item['h']] ########################################################################## # GET def find(self, filename): - ''' + """ Return file object from given filename - ''' + """ files = self.get_files() for file in files.items(): if file[1]['a'] and file[1]['a']['n'] == filename: return file def get_files(self): - ''' + """ Get all files in account - ''' + """ files = self.api_request({'a': 'f', 'c': 1}) files_dict = {} shared_keys = {} - self.init_shared_keys(files,shared_keys) + self.init_shared_keys(files, shared_keys) for file in files['f']: - processed_file = self.process_file(file,shared_keys) + processed_file = self.process_file(file, shared_keys) #ensure each file has a name before returning if processed_file['a']: files_dict[file['h']] = processed_file return files_dict def get_upload_link(self, file): - ''' + """ Get a files public link inc. decrypted key Requires upload() response as input - ''' + """ if 'f' in file: file = file['f'][0] public_handle = self.api_request({'a': 'l', 'n': file['h']}) @@ -216,9 +216,9 @@ def get_upload_link(self, file): use get_link() for regular file input''') def get_link(self, file): - ''' + """ Get a file public link from given file object - ''' + """ file = file[1] if 'h' in file and 'k' in file: public_handle = self.api_request({'a': 'l', 'n': file['h']}) @@ -237,30 +237,30 @@ def get_user(self): return user_data def get_node_by_type(self, type): - ''' + """ Get a node by it's numeric type id, e.g: 0: file 1: dir 2: special: root cloud drive 3: special: inbox 4: special trash bin - ''' + """ nodes = self.get_files() for node in nodes.items(): if (node[1]['t'] == type): return node def get_files_in_node(self, target): - ''' + """ Get all files in a given target, e.g. 4=trash - ''' + """ node_id = self.get_node_by_type(target) files = self.api_request({'a': 'f', 'c': 1}) files_dict = {} shared_keys = {} - self.init_shared_keys(files,shared_keys) + self.init_shared_keys(files, shared_keys) for file in files['f']: - processed_file = self.process_file(file,shared_keys) + processed_file = self.process_file(file, shared_keys) if processed_file['a'] and processed_file['p'] == node_id[0]: files_dict[file['h']] = processed_file return files_dict @@ -278,18 +278,18 @@ def get_id_from_public_handle(self, public_handle): return node_id def get_quota(self): - ''' + """ Get current remaining disk quota in MegaBytes - ''' + """ json_resp = self.api_request({'a': 'uq', 'xfer': 1}) #convert bytes to megabyes - return json_resp['mstrg']/1048576 + return json_resp['mstrg'] / 1048576 def get_balance(self): - ''' + """ Get account monetary balance, Pro accounts only - ''' - user_data = self.api_request({"a":"uq","pro":1}) + """ + user_data = self.api_request({"a": "uq", "pro": 1}) if 'balance' in user_data: return user_data['balance'] @@ -311,6 +311,7 @@ def destroy(self, file_id): return self.api_request({'a': 'd', 'n': file_id, 'i': self.request_id}) + def destroy_url(self, url): #delete a file via it's url path = self.parse_url(url).split('!') @@ -319,7 +320,7 @@ def destroy_url(self, url): return self.destroy(file_id) def move(self, file_id, target): - ''' + """ Move a file to another parent node params: a : command @@ -331,7 +332,7 @@ def move(self, file_id, target): 2 : root 3 : inbox 4 : trash - ''' + """ #determine target_node_id target_node_id = str(self.get_node_by_type(target)[0]) @@ -340,7 +341,6 @@ def move(self, file_id, target): 't': target_node_id, 'i': self.request_id}) - def empty_trash(self): # get list of files in rubbish out files = self.get_files_in_node(4) @@ -357,16 +357,16 @@ def empty_trash(self): ########################################################################## # DOWNLOAD def download(self, file, dest_path=None): - ''' + """ Download a file by it's file object - ''' + """ url = self.get_link(file) self.download_url(url, dest_path) def download_url(self, url, dest_path=None): - ''' + """ Download a file by it's public url - ''' + """ path = self.parse_url(url).split('!') file_id = path[0] file_key = path[1] @@ -452,8 +452,8 @@ def upload(self, filename, dest=None): ul_url = self.api_request({'a': 'u', 's': size})['p'] #generate random aes key (128) for file - ul_key = [random.randint(0, 0xFFFFFFFF) for r in range(6)] - count = Counter.new(128,initial_value=((ul_key[4]<<32)+ul_key[5])<<64) + ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] + count = Counter.new(128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64) aes = AES.new(a32_to_str(ul_key[:4]), AES.MODE_CTR, counter=count) file_mac = [0, 0, 0, 0] @@ -512,7 +512,7 @@ def create_folder(self, name, dest=None): dest = self.root_id #generate random aes key (128) for folder - ul_key = [random.randint(0, 0xFFFFFFFF) for r in range(6)] + ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] #encrypt attribs attribs = {'n': name} From 5b141c958c000275303ff2526b6d6551327ff375 Mon Sep 17 00:00:00 2001 From: richard Date: Mon, 15 Apr 2013 16:15:23 +0100 Subject: [PATCH 055/130] Pip version updated --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c3c3334..8493af5 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.2', + version='0.9.3', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 7e4d2ee557fb89c955fef79f86989e6aefb784d5 Mon Sep 17 00:00:00 2001 From: gissehel Date: Fri, 19 Apr 2013 19:53:57 +0200 Subject: [PATCH 056/130] You can't download files in shared folders using public links because public links can't be obtained from shared files. You need to download them directly. --- mega/mega.py | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index f1be9eb..4d68a52 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -135,6 +135,8 @@ def process_file(self, file, shared_keys): # folder else: k = key + file['key'] = key + file['k'] = k attributes = base64_url_decode(file['a']) attributes = decrypt_attr(attributes, k) file['a'] = attributes @@ -222,9 +224,9 @@ def get_link(self, file): file = file[1] if 'h' in file and 'k' in file: public_handle = self.api_request({'a': 'l', 'n': file['h']}) - file_key = file['k'][file['k'].index(':') + 1:] - decrypted_key = a32_to_base64(decrypt_key(base64_to_a32(file_key), - self.master_key)) + if public_handle == -11 : + raise RequestError("Can't get a public link from that file (is this a shared file?)") + decrypted_key = a32_to_base64(file['key']) return '{0}://{1}/#!{2}!{3}'.format(self.schema, self.domain, public_handle, @@ -360,8 +362,7 @@ def download(self, file, dest_path=None): """ Download a file by it's file object """ - url = self.get_link(file) - self.download_url(url, dest_path) + self.download_file(None, None, file=file[1], dest_path=dest_path, is_public=False) def download_url(self, url, dest_path=None): """ @@ -372,18 +373,29 @@ def download_url(self, url, dest_path=None): file_key = path[1] self.download_file(file_id, file_key, dest_path, is_public=True) - def download_file(self, file_handle, file_key, dest_path=None, is_public=False): - if is_public: - file_key = base64_to_a32(file_key) - file_data = self.api_request({'a': 'g', 'g': 1, 'p': file_handle}) - else: - 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], - file_key[2] ^ file_key[6], file_key[3] ^ file_key[7]) - iv = file_key[4:6] + (0, 0) - meta_mac = file_key[6:8] - + def download_file(self, file_handle, file_key, dest_path=None, is_public=False, file=None): + if file is None : + if is_public: + file_key = base64_to_a32(file_key) + file_data = self.api_request({'a': 'g', 'g': 1, 'p': file_handle}) + else : + 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], + file_key[2] ^ file_key[6], file_key[3] ^ file_key[7]) + iv = file_key[4:6] + (0, 0) + meta_mac = file_key[6:8] + else : + file_data = self.api_request({'a': 'g', 'g': 1, 'n': file['h']}) + k = file['k'] + iv = file['iv'] + meta_mac = file['meta_mac'] + + # Seems to happens sometime... When this occurs, files are + # inaccessible also in the official also in the official webapp. + # Strangely, files can come back later. + if 'g' not in file_data : + raise RequestError('File not accessible anymore') file_url = file_data['g'] file_size = file_data['s'] attribs = base64_url_decode(file_data['at']) From 09c4b14c976c0bff3065f8be70d83ae26514ae60 Mon Sep 17 00:00:00 2001 From: gissehel Date: Fri, 19 Apr 2013 19:58:36 +0200 Subject: [PATCH 057/130] I don't think that printing on stdout is the right thing to do in this api layer. It should be handled by the calling layer shounldn't it ? Was that debug code ? --- mega/mega.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 4d68a52..624aef6 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -402,9 +402,9 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, attribs = decrypt_attr(attribs, k) file_name = attribs['n'] - print("downloading {0} (size: {1}), url = {2}".format(attribs['n'].encode("utf8"), - file_size, - file_url)) + # print("downloading {0} (size: {1}), url = {2}".format(attribs['n'].encode("utf8"), + # file_size, + # file_url)) input_file = requests.get(file_url, stream=True).raw From 89a40306a2ae7ec08b954bec8be3010e4e73062e Mon Sep 17 00:00:00 2001 From: gissehel Date: Fri, 19 Apr 2013 20:10:52 +0200 Subject: [PATCH 058/130] Downloading using a tempory filename is safer. The tempory filename provide enouth information to show who (megapy) when and what is downloaded --- mega/mega.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mega/mega.py b/mega/mega.py index 624aef6..ed539df 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -7,6 +7,8 @@ import random import binascii import requests +import time +import shutil from .errors import ValidationError, RequestError from .crypto import * @@ -401,6 +403,7 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, attribs = base64_url_decode(file_data['at']) attribs = decrypt_attr(attribs, k) file_name = attribs['n'] + file_name_tmp = '.megapy-%s-%s' % (int(time.time()*1000), filename) # print("downloading {0} (size: {1}), url = {2}".format(attribs['n'].encode("utf8"), # file_size, @@ -411,7 +414,7 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, if dest_path: output_file = open(dest_path + '/' + file_name, 'wb') else: - output_file = open(file_name, 'wb') + output_file = open(file_name_tmp, 'wb') counter = Counter.new( 128, initial_value=((iv[0] << 32) + iv[1]) << 64) @@ -448,6 +451,8 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, if (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) != meta_mac: raise ValueError('Mismatched mac') + shutil.move(file_name_tmp, file_name) + ########################################################################## # UPLOAD def upload(self, filename, dest=None): From d1fd89db4c54eb8c80c91ca02c2ceb0cbfa59c33 Mon Sep 17 00:00:00 2001 From: richard Date: Fri, 19 Apr 2013 19:38:45 +0100 Subject: [PATCH 059/130] it's more pep8 :) --- mega/mega.py | 10 +++++----- tests.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index ed539df..a3f7f04 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -226,7 +226,7 @@ def get_link(self, file): file = file[1] if 'h' in file and 'k' in file: 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?)") decrypted_key = a32_to_base64(file['key']) return '{0}://{1}/#!{2}!{3}'.format(self.schema, @@ -380,14 +380,14 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, if is_public: file_key = base64_to_a32(file_key) 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}) k = (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) meta_mac = file_key[6:8] - else : + else: file_data = self.api_request({'a': 'g', 'g': 1, 'n': file['h']}) k = file['k'] iv = file['iv'] @@ -396,14 +396,14 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, # Seems to happens sometime... When this occurs, files are # inaccessible also in the official also in the official webapp. # Strangely, files can come back later. - if 'g' not in file_data : + if 'g' not in file_data: raise RequestError('File not accessible anymore') file_url = file_data['g'] file_size = file_data['s'] attribs = base64_url_decode(file_data['at']) attribs = decrypt_attr(attribs, k) file_name = attribs['n'] - file_name_tmp = '.megapy-%s-%s' % (int(time.time()*1000), filename) + file_name_tmp = '.megapy-%s-%s' % (int(time.time() * 1000), file_name) # print("downloading {0} (size: {1}), url = {2}".format(attribs['n'].encode("utf8"), # file_size, diff --git a/tests.py b/tests.py index 1bf257e..dc3cef0 100644 --- a/tests.py +++ b/tests.py @@ -1,5 +1,6 @@ from mega import Mega + def test(): #user details email = 'your@email.com' @@ -30,7 +31,6 @@ def test(): #search for a file in account file = m.find('tests.py') - if file: #get public link link = m.get_link(file) From 4b0f0b2d274f2d86f0fdc8790c749a3430a23be4 Mon Sep 17 00:00:00 2001 From: richard Date: Fri, 19 Apr 2013 19:39:24 +0100 Subject: [PATCH 060/130] pypi info updated --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8493af5..abcf299 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.3', + version='0.9.4', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 0332d91e1367897f43c46f066481d6c15cc5e18d Mon Sep 17 00:00:00 2001 From: gissehel Date: Fri, 19 Apr 2013 20:59:28 +0200 Subject: [PATCH 061/130] Forgot to change the file_name to file_name_tmp when the destination folder is provided --- mega/mega.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mega/mega.py b/mega/mega.py index a3f7f04..3a92d24 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -412,7 +412,7 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, input_file = requests.get(file_url, stream=True).raw if dest_path: - output_file = open(dest_path + '/' + file_name, 'wb') + output_file = open(dest_path + '/' + file_name_tmp, 'wb') else: output_file = open(file_name_tmp, 'wb') From 964157bf4117ca9a70ea929fdb0e8bdf329703b4 Mon Sep 17 00:00:00 2001 From: richard Date: Fri, 19 Apr 2013 20:04:06 +0100 Subject: [PATCH 062/130] pypi info updated --- mega/mega.py | 4 ---- setup.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 3a92d24..ce369d9 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -405,10 +405,6 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, file_name = attribs['n'] file_name_tmp = '.megapy-%s-%s' % (int(time.time() * 1000), file_name) - # print("downloading {0} (size: {1}), url = {2}".format(attribs['n'].encode("utf8"), - # file_size, - # file_url)) - input_file = requests.get(file_url, stream=True).raw if dest_path: diff --git a/setup.py b/setup.py index abcf299..6d7b98b 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.4', + version='0.9.5', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From dcbd17a6ed8df26b5f7f13ac45c2ebbd68a4f08d Mon Sep 17 00:00:00 2001 From: gissehel Date: Sat, 20 Apr 2013 13:45:06 +0200 Subject: [PATCH 063/130] Explicitly declaring dependency in setup.py make 'pip install mega.py' install everything just fine --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6d7b98b..d4aa889 100644 --- a/setup.py +++ b/setup.py @@ -38,10 +38,11 @@ def get_package_data(package): author_email='richard@richard.do', license='Creative Commons Attribution-Noncommercial-Share Alike license', long_description='https://github.com/richardasaurus/mega.py', + install_requires=['pycrypto', 'requests'], classifiers=[ 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Internet :: WWW/HTTP' ] -) \ No newline at end of file +) From 58d89c23b6d9bc0dde0dc38cae33e7cb5a6fdf8f Mon Sep 17 00:00:00 2001 From: gissehel Date: Sun, 21 Apr 2013 18:53:48 +0200 Subject: [PATCH 064/130] get_storage_space retrieve the used space and total space currently associated with the account --- mega/mega.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/mega/mega.py b/mega/mega.py index ce369d9..8c2667c 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -289,6 +289,29 @@ def get_quota(self): #convert bytes to megabyes return json_resp['mstrg'] / 1048576 + def get_storage_space(self, giga=False, mega=False, kilo=False): + """ + Get the current storage space. + Return a dict containing at least: + 'used' : the used space on the account + 'total' : the maximum space allowed with current plan + All storage space are in bytes unless asked differently. + """ + if sum(1 if x else 0 for x in (kilo, mega, giga)) > 1: + raise ValueError("Only one unit prefix can be specified") + unit_coef = 1 + if kilo: + unit_coef = 1024. + if mega: + unit_coef = 1048576. + if giga: + unit_coef = 1073741824. + json_resp = self.api_request({'a': 'uq', 'xfer': 1, 'strg': 1}) + return { + 'used': json_resp['cstrg'] / unit_coef, + 'total': json_resp['mstrg'] / unit_coef, + } + def get_balance(self): """ Get account monetary balance, Pro accounts only From ced5b616e1edb7e0d184be7efea2a4bfd534f68c Mon Sep 17 00:00:00 2001 From: richard Date: Tue, 23 Apr 2013 19:58:18 +0100 Subject: [PATCH 065/130] comments --- tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests.py b/tests.py index dc3cef0..32f4fba 100644 --- a/tests.py +++ b/tests.py @@ -2,6 +2,12 @@ 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 = 'your@email.com' password = 'password' From 972bfd8852c9f5ff00bf23ef83f484a39e86fc78 Mon Sep 17 00:00:00 2001 From: gissehel Date: Sat, 27 Apr 2013 19:52:09 +0200 Subject: [PATCH 066/130] The code was not robust anymore in weird cases where file['k']=='', it is now. --- mega/mega.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mega/mega.py b/mega/mega.py index ce369d9..a554c6b 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -106,7 +106,7 @@ def process_file(self, file, shared_keys): Process a file """ if file['t'] == 0 or file['t'] == 1: - keys = dict(keypart.split(':', 1) for keypart in file['k'].split('/')) + keys = dict(keypart.split(':', 1) for keypart in file['k'].split('/') if ':' in keypart) uid = file['u'] key = None # my objects From 115fcbced130b9ae25bdafe16f039890cdf78ee9 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 28 Apr 2013 03:55:04 +0100 Subject: [PATCH 067/130] class method bug fix, verbose output option added, download progress --- mega/mega.py | 39 +++++++++++++++++++++++++-------------- setup.py | 2 +- tests.py | 3 ++- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index a554c6b..4c5cfd5 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -11,10 +11,11 @@ import shutil from .errors import ValidationError, RequestError from .crypto import * +import tempfile class Mega(object): - def __init__(self): + def __init__(self, options=None): self.schema = 'https' self.domain = 'mega.co.nz' self.timeout = 160 # max time (secs) to wait for resp from api requests @@ -22,11 +23,14 @@ def __init__(self): self.sequence_num = random.randint(0, 0xFFFFFFFF) self.request_id = make_id(10) - @classmethod - def login(cls, email, password): - instance = cls() - instance.login_user(email, password) - return instance + if options is None: + options = {} + self.options = options + + + def login(self, email, password): + self.login_user(email, password) + return self def login_user(self, email, password): password_aes = prepare_key(str_to_a32(password)) @@ -394,7 +398,7 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, meta_mac = file['meta_mac'] # Seems to happens sometime... When this occurs, files are - # inaccessible also in the official also in the official webapp. + # inaccessible also in the official also in the official web app. # Strangely, files can come back later. if 'g' not in file_data: raise RequestError('File not accessible anymore') @@ -403,14 +407,15 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, attribs = base64_url_decode(file_data['at']) attribs = decrypt_attr(attribs, k) file_name = attribs['n'] - file_name_tmp = '.megapy-%s-%s' % (int(time.time() * 1000), file_name) input_file = requests.get(file_url, stream=True).raw - if dest_path: - output_file = open(dest_path + '/' + file_name_tmp, 'wb') + if dest_path is None: + dest_path = '' else: - output_file = open(file_name_tmp, 'wb') + dest_path += '/' + + temp_output_file = tempfile.NamedTemporaryFile(mode='w+b', prefix='megapy_', delete=False) counter = Counter.new( 128, initial_value=((iv[0] << 32) + iv[1]) << 64) @@ -420,7 +425,7 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, for chunk_start, chunk_size in sorted(get_chunks(file_size).items()): chunk = input_file.read(chunk_size) chunk = aes.decrypt(chunk) - output_file.write(chunk) + temp_output_file.write(chunk) chunk_mac = [iv[0], iv[1], iv[0], iv[1]] for i in range(0, len(chunk), 16): @@ -441,13 +446,19 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]] file_mac = aes_cbc_encrypt_a32(file_mac, k) - output_file.close() + + if self.options.get('verbose') is True: + # temp file size + file_info = os.stat(temp_output_file.name) + print('{0} of {1} downloaded'.format(file_info.st_size, file_size)) + + temp_output_file.close() # check mac integrity if (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) != meta_mac: raise ValueError('Mismatched mac') - shutil.move(file_name_tmp, file_name) + shutil.move(temp_output_file.name, dest_path + file_name) ########################################################################## # UPLOAD diff --git a/setup.py b/setup.py index 6d7b98b..b64a58c 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.5', + version='0.9.6', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', diff --git a/tests.py b/tests.py index 32f4fba..2401232 100644 --- a/tests.py +++ b/tests.py @@ -13,6 +13,7 @@ def test(): password = 'password' mega = Mega() + #mega = Mega({'verbose': True}) # verbose option for print output #login m = mega.login(email, password) @@ -43,7 +44,7 @@ def test(): print(link) #download file. by file object or url - m.download(file, '/tmp') + print m.download(file, '/tmp') #m.download_url(link) #delete or destroy file. by id or url From d8df8bd76e5ca838912dca2a9c22f10fcc67be82 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 28 Apr 2013 04:00:42 +0100 Subject: [PATCH 068/130] read me updated --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5a2eb87..9bf1be7 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ from mega import Mega ### Create an instance of Mega.py ```python mega = Mega() +# add the verbose option for print output on some functions +mega = Mega({'verbose': True}) ``` ### Login to Mega ```python From 675ff6bbc5478b604098672c893ebf0705761bc6 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 28 Apr 2013 16:33:50 +0100 Subject: [PATCH 069/130] Added upload progress --- mega/mega.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 4c5cfd5..a9852e4 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -472,8 +472,8 @@ def upload(self, filename, dest=None): #request upload url, call 'u' method input_file = open(filename, 'rb') - size = os.path.getsize(filename) - ul_url = self.api_request({'a': 'u', 's': size})['p'] + file_size = os.path.getsize(filename) + ul_url = self.api_request({'a': 'u', 's': file_size})['p'] #generate random aes key (128) for file ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] @@ -481,8 +481,10 @@ def upload(self, filename, dest=None): aes = AES.new(a32_to_str(ul_key[:4]), AES.MODE_CTR, counter=count) file_mac = [0, 0, 0, 0] - for chunk_start, chunk_size in sorted(get_chunks(size).items()): + upload_progress = 0 + for chunk_start, chunk_size in sorted(get_chunks(file_size).items()): chunk = input_file.read(chunk_size) + upload_progress += len(chunk) #determine chunks mac chunk_mac = [ul_key[4], ul_key[5], ul_key[4], ul_key[5]] @@ -508,6 +510,10 @@ def upload(self, filename, dest=None): data=chunk, timeout=self.timeout) completion_file_handle = output_file.text + if self.options.get('verbose') is True: + # upload progress + print('{0} of {1} uploaded'.format(upload_progress, file_size)) + #determine meta mac meta_mac = (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) From d15be6deff966d094e4140629b554459eabd7f9a Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 28 Apr 2013 16:56:17 +0100 Subject: [PATCH 070/130] read me update and tests --- README.md | 5 +++++ mega/mega.py | 6 +++--- tests.py | 22 ++++++++++++---------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9bf1be7..8902f99 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,11 @@ balance = m.get_balance() ```python quota = m.get_quota() ``` +### Get account storage space +```python +# unit output kilo, mega, gig, otherwise bytes output +space = m.get_storage_space(kilo=True) +``` ### Get account files ```python files = m.get_files() diff --git a/mega/mega.py b/mega/mega.py index 75955cf..8f070cc 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -305,11 +305,11 @@ def get_storage_space(self, giga=False, mega=False, kilo=False): raise ValueError("Only one unit prefix can be specified") unit_coef = 1 if kilo: - unit_coef = 1024. + unit_coef = 1024 if mega: - unit_coef = 1048576. + unit_coef = 1048576 if giga: - unit_coef = 1073741824. + unit_coef = 1073741824 json_resp = self.api_request({'a': 'uq', 'xfer': 1, 'strg': 1}) return { 'used': json_resp['cstrg'] / unit_coef, diff --git a/tests.py b/tests.py index 2401232..12897a3 100644 --- a/tests.py +++ b/tests.py @@ -15,35 +15,37 @@ def test(): mega = Mega() #mega = Mega({'verbose': True}) # verbose option for print output - #login + # login m = mega.login(email, password) - #get user details + # get user details details = m.get_user() print(details) - #get account files + # get account files files = m.get_files() - #get account disk quota in MB + # get account disk quota in MB print(m.get_quota()) + # get account storage space + print(m.get_storage_space()) - #example iterate over files + # example iterate over files for file in files: print(files[file]) - #upload file + # upload file print(m.upload('tests.py')) - #search for a file in account + # search for a file in account file = m.find('tests.py') if file: - #get public link + # get public link link = m.get_link(file) print(link) - #download file. by file object or url + # download file. by file object or url print m.download(file, '/tmp') #m.download_url(link) @@ -53,7 +55,7 @@ def test(): #print(m.delete_url(link)) #print(m.destroy_url(link)) - #empty trash + # empty trash print(m.empty_trash()) if __name__ == '__main__': From dd9f74b65f5d15e82b57ea79ba901214778e4a15 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 28 Apr 2013 16:56:36 +0100 Subject: [PATCH 071/130] pip version updated --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 98babda..fa5e489 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.6', + version='0.9.8', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 200eaed6b857d16a19144ab987feb3caee4826a5 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 28 Apr 2013 16:58:35 +0100 Subject: [PATCH 072/130] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8902f99..bb83ae3 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ quota = m.get_quota() ``` ### Get account storage space ```python -# unit output kilo, mega, gig, otherwise bytes output +# specify unit output kilo, mega, gig, else bytes will output space = m.get_storage_space(kilo=True) ``` ### Get account files From b2e1d35d49c6acfefdf1f972e43faf7ffa1c7c1a Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 28 Apr 2013 17:00:19 +0100 Subject: [PATCH 073/130] removed file --- CONTRIBUTORS | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 CONTRIBUTORS diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 56116ee..0000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,4 +0,0 @@ -cyberjujum -richardasaurus -jlejeune -issues: zbahoui,alyssarowan,kionez From 0d2e91996e007e4b40d20d982b7c3158235ec242 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 28 Apr 2013 17:08:47 +0100 Subject: [PATCH 074/130] PEP8 style fixes --- mega/mega.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 8f070cc..670f138 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -27,7 +27,6 @@ def __init__(self, options=None): options = {} self.options = options - def login(self, email, password): self.login_user(email, password) return self @@ -305,16 +304,16 @@ def get_storage_space(self, giga=False, mega=False, kilo=False): raise ValueError("Only one unit prefix can be specified") unit_coef = 1 if kilo: - unit_coef = 1024 + unit_coef = 1024 if mega: - unit_coef = 1048576 + unit_coef = 1048576 if giga: - unit_coef = 1073741824 + unit_coef = 1073741824 json_resp = self.api_request({'a': 'uq', 'xfer': 1, 'strg': 1}) return { 'used': json_resp['cstrg'] / unit_coef, 'total': json_resp['mstrg'] / unit_coef, - } + } def get_balance(self): """ From 3b9b88e1c2c168106700c5f00d34355309749cc9 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 1 May 2013 20:56:33 +0100 Subject: [PATCH 075/130] Issue #31 fix --- mega/mega.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mega/mega.py b/mega/mega.py index 670f138..d588b56 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -504,6 +504,7 @@ def upload(self, filename, dest=None): file_mac = [0, 0, 0, 0] upload_progress = 0 + completion_file_handle = None for chunk_start, chunk_size in sorted(get_chunks(file_size).items()): chunk = input_file.read(chunk_size) upload_progress += len(chunk) From 15cb6e06f04adb4fee89bf50714a4ff42d807603 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 1 May 2013 20:56:58 +0100 Subject: [PATCH 076/130] pip version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fa5e489..3ed4180 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.8', + version='0.9.9', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 40f9fb95c2015dbd29a75624fa544f049bb2b4a7 Mon Sep 17 00:00:00 2001 From: gissehel Date: Sun, 5 May 2013 00:55:52 +0200 Subject: [PATCH 077/130] Renaming file and folders support --- README.md | 6 ++++++ mega/mega.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/README.md b/README.md index bb83ae3..2b6cad3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Python library for the Mega.co.nz API, currently supporting: - deleting - searching - sharing + - renaming This is a work in progress, further functionality coming shortly. @@ -68,6 +69,11 @@ m.download(file, '/home/john-smith/Desktop') ```python m.create_folder('new_folder') ``` +### Rename a file or a folder +```python +file = m.find('myfile.doc') +m.rename(file, 'my_file.doc') +``` ### Search account for a file, and get its public link ```python file = m.find('myfile.doc') diff --git a/mega/mega.py b/mega/mega.py index d588b56..73b66ae 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -584,3 +584,22 @@ def create_folder(self, name, dest=None): 'i': self.request_id}) #return API msg return data + + def rename(self, file, new_name): + file = file[1] + #create new attribs + attribs = {'n': new_name} + #encrypt attribs + encrypt_attribs = base64_url_encode(encrypt_attr(attribs, file['k'])) + encrypted_key = a32_to_base64(encrypt_key(file['key'], self.master_key)) + + #update attributes + data = self.api_request([{ + 'a': 'a', + 'attr': encrypt_attribs, + 'key': encrypted_key, + 'n': file['h'], + 'i': self.request_id}]) + + #return API msg + return data From 1a349e312609789e0e1f1e3be4ff77b603e4df14 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 5 May 2013 03:12:49 +0100 Subject: [PATCH 078/130] Comment --- mega/mega.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 73b66ae..d6ac841 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -326,24 +326,32 @@ def get_balance(self): ########################################################################## # DELETE def delete(self, public_handle): - #straight delete by id + """ + Delete a file by its public handle + """ return self.move(public_handle, 4) def delete_url(self, url): - #delete a file via it's url + """ + Delete a file by its url + """ path = self.parse_url(url).split('!') public_handle = path[0] file_id = self.get_id_from_public_handle(public_handle) return self.move(file_id, 4) def destroy(self, file_id): - #delete forever by private id + """ + Destroy a file by its private id + """ return self.api_request({'a': 'd', 'n': file_id, 'i': self.request_id}) def destroy_url(self, url): - #delete a file via it's url + """ + Destroy a file by its url + """ path = self.parse_url(url).split('!') public_handle = path[0] file_id = self.get_id_from_public_handle(public_handle) From 24f44137a1cca6fb475e51f60dd86cf5fd559ec9 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 5 May 2013 03:43:30 +0100 Subject: [PATCH 079/130] pip updated --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3ed4180..77cda72 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.9', + version='0.9.10', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From b9ec62cbb8ef3e38a513189cf28c045adb0a9777 Mon Sep 17 00:00:00 2001 From: gissehel Date: Sun, 5 May 2013 09:16:40 +0200 Subject: [PATCH 080/130] .move(...) can now move into any folder --- README.md | 6 ++++++ mega/mega.py | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b6cad3..a065020 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,12 @@ m.create_folder('new_folder') file = m.find('myfile.doc') m.rename(file, 'my_file.doc') ``` +### Moving a file or a folder into another folder +```python +file = m.find('myfile.doc') +folder = m.find('myfolder') +m.move(file[0], folder) +``` ### Search account for a file, and get its public link ```python file = m.find('myfile.doc') diff --git a/mega/mega.py b/mega/mega.py index d6ac841..4d049ec 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -370,10 +370,24 @@ def move(self, file_id, target): 2 : root 3 : inbox 4 : trash + + or + + target's id + + or + + target's structure returned by find """ #determine target_node_id - target_node_id = str(self.get_node_by_type(target)[0]) + if type(target) == int: + target_node_id = str(self.get_node_by_type(target)[0]) + elif type(target) in (str,unicode): + target_node_id = target + else: + file = target[1] + target_node_id = file['h'] return self.api_request({'a': 'm', 'n': file_id, 't': target_node_id, From c453a88b28256527c52cb4fa5129ed0b2ecd41bf Mon Sep 17 00:00:00 2001 From: gissehel Date: Sun, 5 May 2013 19:48:23 +0200 Subject: [PATCH 081/130] Big optimisation on download code for mac checking --- mega/crypto.py | 28 ++++++++-------------------- mega/mega.py | 40 ++++++++++++++++++---------------------- 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/mega/crypto.py b/mega/crypto.py index 8a91356..c439971 100644 --- a/mega/crypto.py +++ b/mega/crypto.py @@ -108,26 +108,14 @@ def a32_to_base64(a): def get_chunks(size): - chunks = {} - p = pp = 0 - i = 1 - - while i <= 8 and p < size - i * 0x20000: - chunks[p] = i * 0x20000 - pp = p - p += chunks[p] - i += 1 - - while p < size: - chunks[p] = 0x100000 - pp = p - p += chunks[p] - - chunks[pp] = size - pp - if not chunks[pp]: - del chunks[pp] - - return chunks + p = 0 + s = 0x20000 + while p+s < size: + yield(p, s) + p += s + if s < 0x100000: + s += 0x20000 + yield(p, size-p) # more general functions def make_id(length): diff --git a/mega/mega.py b/mega/mega.py index d6ac841..24ef1ae 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -447,41 +447,37 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, temp_output_file = tempfile.NamedTemporaryFile(mode='w+b', prefix='megapy_', delete=False) + k_str = a32_to_str(k) counter = Counter.new( 128, initial_value=((iv[0] << 32) + iv[1]) << 64) - aes = AES.new(a32_to_str(k), AES.MODE_CTR, counter=counter) + aes = AES.new(k_str, AES.MODE_CTR, counter=counter) - file_mac = (0, 0, 0, 0) - for chunk_start, chunk_size in sorted(get_chunks(file_size).items()): + mac_str = '\0' * 16 + mac_encryptor = AES.new(k_str, AES.MODE_CBC, mac_str) + iv_str = a32_to_str([iv[0], iv[1], iv[0], iv[1]]) + + for chunk_start, chunk_size in get_chunks(file_size): chunk = input_file.read(chunk_size) chunk = aes.decrypt(chunk) temp_output_file.write(chunk) - chunk_mac = [iv[0], iv[1], iv[0], iv[1]] - for i in range(0, len(chunk), 16): + encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) + for i in range(0, len(chunk)-16, 16): block = chunk[i:i + 16] - if len(block) % 16: - block += '\0' * (16 - (len(block) % 16)) - block = str_to_a32(block) - chunk_mac = [ - chunk_mac[0] ^ block[0], - chunk_mac[1] ^ block[1], - chunk_mac[2] ^ block[2], - chunk_mac[3] ^ block[3]] - chunk_mac = aes_cbc_encrypt_a32(chunk_mac, k) - - file_mac = [ - file_mac[0] ^ chunk_mac[0], - file_mac[1] ^ chunk_mac[1], - file_mac[2] ^ chunk_mac[2], - file_mac[3] ^ chunk_mac[3]] - file_mac = aes_cbc_encrypt_a32(file_mac, k) + encryptor.encrypt(block) + i += 16 + block = chunk[i:i + 16] + if len(block) % 16: + block += '\0' * (16 - (len(block) % 16)) + mac_str = mac_encryptor.encrypt(encryptor.encrypt(block)) if self.options.get('verbose') is True: # temp file size file_info = os.stat(temp_output_file.name) print('{0} of {1} downloaded'.format(file_info.st_size, file_size)) + file_mac = str_to_a32(mac_str) + temp_output_file.close() # check mac integrity @@ -513,7 +509,7 @@ def upload(self, filename, dest=None): file_mac = [0, 0, 0, 0] upload_progress = 0 completion_file_handle = None - for chunk_start, chunk_size in sorted(get_chunks(file_size).items()): + for chunk_start, chunk_size in get_chunks(file_size): chunk = input_file.read(chunk_size) upload_progress += len(chunk) From c2befcb87ff4d7775270e1a007087df93c28ff0a Mon Sep 17 00:00:00 2001 From: gissehel Date: Sun, 5 May 2013 20:29:41 +0200 Subject: [PATCH 082/130] Speedup the mac calculation on upload --- mega/mega.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 24ef1ae..adb148b 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -503,33 +503,30 @@ def upload(self, filename, dest=None): #generate random aes key (128) for file ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] + k_str = a32_to_str(ul_key[:4]) count = Counter.new(128, initial_value=((ul_key[4] << 32) + ul_key[5]) << 64) - aes = AES.new(a32_to_str(ul_key[:4]), AES.MODE_CTR, counter=count) + aes = AES.new(k_str, AES.MODE_CTR, counter=count) - file_mac = [0, 0, 0, 0] upload_progress = 0 completion_file_handle = None + + mac_str = '\0' * 16 + mac_encryptor = AES.new(k_str, AES.MODE_CBC, mac_str) + iv_str = a32_to_str([ul_key[4], ul_key[5], ul_key[4], ul_key[5]]) + for chunk_start, chunk_size in get_chunks(file_size): chunk = input_file.read(chunk_size) upload_progress += len(chunk) - #determine chunks mac - chunk_mac = [ul_key[4], ul_key[5], ul_key[4], ul_key[5]] - for i in range(0, len(chunk), 16): + encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) + for i in range(0, len(chunk)-16, 16): block = chunk[i:i + 16] - if len(block) % 16: - block += '\0' * (16 - len(block) % 16) - block = str_to_a32(block) - chunk_mac = [chunk_mac[0] ^ block[0], chunk_mac[1] ^ block[1], - chunk_mac[2] ^ block[2], - chunk_mac[3] ^ block[3]] - chunk_mac = aes_cbc_encrypt_a32(chunk_mac, ul_key[:4]) - - #our files mac - file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1], - file_mac[2] ^ chunk_mac[2], - file_mac[3] ^ chunk_mac[3]] - file_mac = aes_cbc_encrypt_a32(file_mac, ul_key[:4]) + encryptor.encrypt(block) + i += 16 + block = chunk[i:i + 16] + if len(block) % 16: + block += '\0' * (16 - len(block) % 16) + mac_str = mac_encryptor.encrypt(encryptor.encrypt(block)) #encrypt file and upload chunk = aes.encrypt(chunk) @@ -541,6 +538,8 @@ def upload(self, filename, dest=None): # upload progress print('{0} of {1} uploaded'.format(upload_progress, file_size)) + file_mac = str_to_a32(mac_str) + #determine meta mac meta_mac = (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) From 5df8b71b7152d6a7f14ab223a63274ba72b1f697 Mon Sep 17 00:00:00 2001 From: Christian Kornacker Date: Mon, 6 May 2013 13:01:19 +0200 Subject: [PATCH 083/130] allow setting file names on upload and download --- mega/mega.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index d6ac841..caa5c7d 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -394,22 +394,22 @@ def empty_trash(self): ########################################################################## # DOWNLOAD - def download(self, file, dest_path=None): + def download(self, file, dest_path=None, dest_file=None): """ Download a file by it's file object """ - self.download_file(None, None, file=file[1], dest_path=dest_path, is_public=False) + self.download_file(None, None, file=file[1], dest_path=dest_path, dest_file=dest_file, is_public=False) - def download_url(self, url, dest_path=None): + def download_url(self, url, dest_path=None, dest_file=None): """ Download a file by it's public url """ path = self.parse_url(url).split('!') file_id = path[0] file_key = path[1] - self.download_file(file_id, file_key, dest_path, is_public=True) + self.download_file(file_id, file_key, dest_path, dest_file, is_public=True) - def download_file(self, file_handle, file_key, dest_path=None, is_public=False, file=None): + def download_file(self, file_handle, file_key, dest_path=None, dest_file=None, is_public=False, file=None): if file is None : if is_public: file_key = base64_to_a32(file_key) @@ -436,7 +436,11 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, file_size = file_data['s'] attribs = base64_url_decode(file_data['at']) attribs = decrypt_attr(attribs, k) - file_name = attribs['n'] + + if dest_file is not None: + file_name = dest_file + else: + file_name = attribs['n'] input_file = requests.get(file_url, stream=True).raw @@ -492,7 +496,7 @@ def download_file(self, file_handle, file_key, dest_path=None, is_public=False, ########################################################################## # UPLOAD - def upload(self, filename, dest=None): + def upload(self, filename, dest=None, remote=None): #determine storage node if dest is None: #if none set, upload to cloud drive node @@ -548,7 +552,11 @@ def upload(self, filename, dest=None): #determine meta mac meta_mac = (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) - attribs = {'n': os.path.basename(filename)} + if remote is not None: + attribs = {'n': remote} + else: + attribs = {'n': os.path.basename(filename)} + encrypt_attribs = base64_url_encode(encrypt_attr(attribs, ul_key[:4])) key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], ul_key[2] ^ meta_mac[0], ul_key[3] ^ meta_mac[1], From 28f69e197796f72377dc010782409635bea87a3f Mon Sep 17 00:00:00 2001 From: Christian Kornacker Date: Mon, 6 May 2013 15:04:02 +0200 Subject: [PATCH 084/130] enable directory listing with get_files_in_node --- mega/mega.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mega/mega.py b/mega/mega.py index d6ac841..d55f2a9 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -261,7 +261,12 @@ def get_files_in_node(self, target): """ Get all files in a given target, e.g. 4=trash """ - node_id = self.get_node_by_type(target) + if target in xrange(0, 4): + # convert special nodes + node_id = self.get_node_by_type(target) + else: + node_id = [target] + files = self.api_request({'a': 'f', 'c': 1}) files_dict = {} shared_keys = {} From 08b76bacedafc89adc761ac2cb35f0564b8d87dd Mon Sep 17 00:00:00 2001 From: Christian Kornacker Date: Mon, 6 May 2013 17:50:40 +0200 Subject: [PATCH 085/130] split get_id_from_public_handle --- mega/mega.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mega/mega.py b/mega/mega.py index d6ac841..9d82a4b 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -276,9 +276,13 @@ def get_id_from_public_handle(self, public_handle): #get node data #TODO fix this function node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) - node_id = None + return self.get_id_from_node_data(node_data) + + def get_id_from_node_data(self, node_data): #determine node id + node_id = None + for i in node_data['f']: if i['h'] is not u'': node_id = i['h'] From 20d865ed1d344ae3164a00caa7bf59427e554564 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 11:26:25 +0100 Subject: [PATCH 086/130] small restructuring --- mega/mega.py | 69 +++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 416523e..7597739 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -357,42 +357,6 @@ def destroy_url(self, url): file_id = self.get_id_from_public_handle(public_handle) return self.destroy(file_id) - def move(self, file_id, target): - """ - Move a file to another parent node - params: - a : command - n : node we're moving - t : id of target parent node, moving to - i : request id - - targets - 2 : root - 3 : inbox - 4 : trash - - or - - target's id - - or - - target's structure returned by find - """ - - #determine target_node_id - if type(target) == int: - target_node_id = str(self.get_node_by_type(target)[0]) - elif type(target) in (str,unicode): - target_node_id = target - else: - file = target[1] - target_node_id = file['h'] - return self.api_request({'a': 'm', - 'n': file_id, - 't': target_node_id, - 'i': self.request_id}) - def empty_trash(self): # get list of files in rubbish out files = self.get_files_in_node(4) @@ -620,3 +584,36 @@ def rename(self, file, new_name): #return API msg return data + + def move(self, file_id, target): + """ + Move a file to another parent node + params: + a : command + n : node we're moving + t : id of target parent node, moving to + i : request id + + targets + 2 : root + 3 : inbox + 4 : trash + + or... + target's id + or... + target's structure returned by find() + """ + + #determine target_node_id + if type(target) == int: + target_node_id = str(self.get_node_by_type(target)[0]) + elif type(target) in (str, unicode): + target_node_id = target + else: + file = target[1] + target_node_id = file['h'] + return self.api_request({'a': 'm', + 'n': file_id, + 't': target_node_id, + 'i': self.request_id}) From 7788e438becc10c66b8585a8f88edc6cc3c7e76f Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 11:27:27 +0100 Subject: [PATCH 087/130] adding moving to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a065020..ca002a3 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Python library for the Mega.co.nz API, currently supporting: - searching - sharing - renaming + - moving files This is a work in progress, further functionality coming shortly. From 9727ca985f73109092d6a6dfe9274fa530702c01 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 11:41:23 +0100 Subject: [PATCH 088/130] renamed vars for specifying upload(),download() dest filenames --- mega/mega.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index d2b83c7..fc3c503 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -372,22 +372,22 @@ def empty_trash(self): ########################################################################## # DOWNLOAD - def download(self, file, dest_path=None, dest_file=None): + def download(self, file, dest_path=None, dest_filename=None): """ Download a file by it's file object """ - self.download_file(None, None, file=file[1], dest_path=dest_path, dest_file=dest_file, 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_file=None): + def download_url(self, url, dest_path=None, dest_filename=None): """ Download a file by it's public url """ path = self.parse_url(url).split('!') file_id = path[0] file_key = path[1] - self.download_file(file_id, file_key, dest_path, dest_file, 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_file=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 is_public: file_key = base64_to_a32(file_key) @@ -415,8 +415,8 @@ def download_file(self, file_handle, file_key, dest_path=None, dest_file=None, i attribs = base64_url_decode(file_data['at']) attribs = decrypt_attr(attribs, k) - if dest_file is not None: - file_name = dest_file + if dest_filename is not None: + file_name = dest_filename else: file_name = attribs['n'] @@ -470,7 +470,7 @@ def download_file(self, file_handle, file_key, dest_path=None, dest_file=None, i ########################################################################## # UPLOAD - def upload(self, filename, dest=None, remote=None): + def upload(self, filename, dest=None, dest_filename=None): #determine storage node if dest is None: #if none set, upload to cloud drive node @@ -525,8 +525,8 @@ def upload(self, filename, dest=None, remote=None): #determine meta mac meta_mac = (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) - if remote is not None: - attribs = {'n': remote} + if dest_filename is not None: + attribs = {'n': dest_filename} else: attribs = {'n': os.path.basename(filename)} From b43c3869a375c151f8bb1fbb39819078d1ea790b Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 12:17:24 +0100 Subject: [PATCH 089/130] get_files_in_node() special target check --- mega/mega.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 5a881b3..555ec27 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -261,8 +261,9 @@ def get_files_in_node(self, target): """ Get all files in a given target, e.g. 4=trash """ - if target in xrange(0, 4): - # convert special nodes + + if type(target) == int: + # convert special nodes (e.g. trash) node_id = self.get_node_by_type(target) else: node_id = [target] From 728b88c1ffa730ec88ec9cab6c1198d4df289fcf Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 12:32:45 +0100 Subject: [PATCH 090/130] updated readme --- README.md | 3 +++ mega/mega.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ca002a3..7210693 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ files = m.get_files() ```python file = m.upload('myfile.doc') m.get_upload_link(file) +# see mega.py for destination and filename options ``` ### Download a file from URL or file obj, optionally specify destination folder ```python @@ -65,6 +66,8 @@ 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 +m.download(file, '/home/john-smith/Desktop', 'myfile.zip') ``` ### Create a folder ```python diff --git a/mega/mega.py b/mega/mega.py index 555ec27..c46ce67 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -261,7 +261,6 @@ def get_files_in_node(self, target): """ Get all files in a given target, e.g. 4=trash """ - if type(target) == int: # convert special nodes (e.g. trash) node_id = self.get_node_by_type(target) From d00b34d76ae801ced6b5939d41224dba12e7cdd9 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 12:34:14 +0100 Subject: [PATCH 091/130] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7210693..9a3a74d 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ 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 +# specify optional download filename (download_url() supports this too) m.download(file, '/home/john-smith/Desktop', 'myfile.zip') ``` ### Create a folder From 7835c732689628e25b66fd22dfbd1028eaa73674 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 12:34:23 +0100 Subject: [PATCH 092/130] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a3a74d..4e224a8 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ 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 too) +# specify optional download filename (download_url() supports this also) m.download(file, '/home/john-smith/Desktop', 'myfile.zip') ``` ### Create a folder From 32d9c43704e2fa026611e57c38a0e44595ab6243 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 12:41:50 +0100 Subject: [PATCH 093/130] get_id_from_node_data -> get_id_from_obj --- mega/mega.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 80fed56..6681134 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -282,9 +282,9 @@ def get_id_from_public_handle(self, public_handle): #TODO fix this function node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) - return self.get_id_from_node_data(node_data) + return self.get_id_from_obj(node_data) - def get_id_from_node_data(self, node_data): + def get_id_from_obj(self, node_data): #determine node id node_id = None From 5c77818997c446ae18fd9289b782af54d8027a73 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 12:46:59 +0100 Subject: [PATCH 094/130] todo removed --- mega/mega.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 6681134..2ea0470 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -279,10 +279,9 @@ def get_files_in_node(self, target): def get_id_from_public_handle(self, public_handle): #get node data - #TODO fix this function node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) - - return self.get_id_from_obj(node_data) + node_id = self.get_id_from_obj(node_data) + return node_id def get_id_from_obj(self, node_data): #determine node id From bd803b62ee405c743e3ba22e3b25370a2a2e8f3b Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 12:48:56 +0100 Subject: [PATCH 095/130] comments --- mega/mega.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mega/mega.py b/mega/mega.py index 2ea0470..b79165c 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -284,7 +284,9 @@ def get_id_from_public_handle(self, public_handle): return node_id def get_id_from_obj(self, node_data): - #determine node id + """ + Get node id from a file object + """ node_id = None for i in node_data['f']: From 52a2185274113ca0320e5aaa3408ea76c47ddeb5 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 13:12:04 +0100 Subject: [PATCH 096/130] 'other' code section --- mega/mega.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mega/mega.py b/mega/mega.py index b79165c..b7c27e3 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -555,6 +555,8 @@ def upload(self, filename, dest=None, dest_filename=None): input_file.close() return data + ########################################################################## + # OTHER OPERATIONS def create_folder(self, name, dest=None): #determine storage node if dest is None: From 457a542c03f0d3e0301b6879d20035c2a6be8d51 Mon Sep 17 00:00:00 2001 From: richard Date: Wed, 8 May 2013 15:36:20 +0100 Subject: [PATCH 097/130] added add_contact() --- mega/mega.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/mega/mega.py b/mega/mega.py index b7c27e3..0db686f 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -637,3 +637,31 @@ def move(self, file_id, target): 'n': file_id, 't': target_node_id, 'i': self.request_id}) + + def add_contact(self, email): + """ + Add another user to your mega contact list + """ + if not re.match(r"[^@]+@[^@]+\.[^@]+", email): + ValidationError('add_contact requires a valid email address') + else: + return self.api_request({'a': 'ur', + 'u': email, + 'l': "1", + 'i': self.request_id}) + + def get_contacts(self): + raise NotImplementedError() + # TODO implement this + # sn param below = maxaction var with function getsc() in mega.co.nz js + # 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 + + #req = requests.post( + #'{0}://g.api.{1}/sc'.format(self.schema, self.domain), + # params={'sn': 'ilRTiY7r35I'}, + # data=json.dumps(None), + # timeout=self.timeout) + #json_resp = json.loads(req.text) + #print json_resp \ No newline at end of file From 8466a04efec9aba6477cc6ed4b358d900c91746f Mon Sep 17 00:00:00 2001 From: richard Date: Fri, 10 May 2013 18:32:30 +0100 Subject: [PATCH 098/130] version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 77cda72..fa02f31 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.10', + version='0.9.11', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From e490bf74d3923c5ec76311149044cb84f7c19973 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 11 May 2013 21:45:43 +0100 Subject: [PATCH 099/130] issue #44 manually added as would not merge. small spelling changes --- README.md | 6 ++++ mega/mega.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e224a8..1cf5a75 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,12 @@ 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') diff --git a/mega/mega.py b/mega/mega.py index 0db686f..17ab33d 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -664,4 +664,81 @@ def get_contacts(self): # data=json.dumps(None), # timeout=self.timeout) #json_resp = json.loads(req.text) - #print json_resp \ No newline at end of file + #print json_resp + + def get_public_url_info(self, url): + """ + Get size and name from a public url, dict returned + """ + file_handle, file_key = self.parse_url(url).split('!') + return self.get_public_file_info(file_handle, file_key) + + def import_public_url(self, url, dest_node=None, dest_name=None): + """ + Import the public url into user account + """ + 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) + + def get_public_file_info(self, file_handle, file_key): + """ + Get size and name of a public file + """ + data = self.api_request({ + 'a': 'g', + 'p': file_handle, + 'ssm': 1}) + + #if numeric error code response + if isinstance(data, int): + raise RequestError(data) + + if 'at' not in data or 's' not in data: + raise ValueError("Unexpected result", data) + + key = base64_to_a32(file_key) + k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) + + size = data['s'] + unencrypted_attrs = decrypt_attr(base64_url_decode(data['at']), k) + if not(unencrypted_attrs): + return None + + result = { + 'size': size, + 'name': unencrypted_attrs['n']} + + return result + + def import_public_file(self, file_handle, file_key, dest_node=None, dest_name=None): + """ + Import the public file into user account + """ + + # Providing dest_node spare an API call to retrieve it. + if dest_node is None: + # Get '/Cloud Drive' folder no dest node specified + dest_node = self.get_node_by_type(2)[1] + + # Providing dest_name spares an API call to retrieve it. + if dest_name is None: + pl_info = self.get_public_file_info(file_handle, file_key) + dest_name = pl_info['name'] + + key = base64_to_a32(file_key) + 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_name = base64_url_encode(encrypt_attr({'n': dest_name}, k)) + + data = self.api_request({ + 'a': 'p', + 't': dest_node['h'], + 'n': [{ + 'ph': file_handle, + 't': 0, + 'a': encrypted_name, + 'k': encrypted_key}]}) + + #return API msg + return data From 878f095056b31d5f675aa96fecb59ffcaa180143 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 11 May 2013 21:47:11 +0100 Subject: [PATCH 100/130] pip version updated --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fa02f31..886dfc9 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.11', + version='0.9.12', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 2e0b1f1200c1685b17e77b299f48ce6eae68c2ee Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 11 May 2013 22:56:25 +0100 Subject: [PATCH 101/130] add/remove contacts --- README.md | 6 ++++++ mega/mega.py | 23 +++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1cf5a75..21af51f 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,12 @@ files = m.find('myfile.doc') if files: m.delete(files[0]) ``` +### Add/remove contacts +```python +m.add_contact('test@email.com') +m.remove_contact('test@email.com') +``` + ## Requirements 1. Python2.7+ diff --git a/mega/mega.py b/mega/mega.py index 17ab33d..2a3f0d1 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -642,12 +642,31 @@ def add_contact(self, email): """ Add another user to your mega contact list """ + return self._edit_contact(email, True) + + def remove_contact(self, email): + """ + Remove a user to your mega contact list + """ + return self._edit_contact(email, False) + + def _edit_contact(self, email, add): + """ + Editing contacts + """ + if add is True: + l = '1' # add command + elif add is False: + l = '0' # remove command + else: + raise ValidationError('add parameter must be of type bool') + if not re.match(r"[^@]+@[^@]+\.[^@]+", email): ValidationError('add_contact requires a valid email address') else: return self.api_request({'a': 'ur', 'u': email, - 'l': "1", + 'l': l, 'i': self.request_id}) def get_contacts(self): @@ -660,7 +679,7 @@ def get_contacts(self): #req = requests.post( #'{0}://g.api.{1}/sc'.format(self.schema, self.domain), - # params={'sn': 'ilRTiY7r35I'}, + # params={'sn': 'ZMxcQ_DmHnM', 'ssl': '1'}, # data=json.dumps(None), # timeout=self.timeout) #json_resp = json.loads(req.text) From 913e128b5dcca0cda21d99c6cfd851a97f036ea8 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 11 May 2013 22:56:40 +0100 Subject: [PATCH 102/130] version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 886dfc9..ef198bb 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.12', + version='0.9.13', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 574169c8b66a248aa16902793f602dcc3a576de1 Mon Sep 17 00:00:00 2001 From: richard Date: Sun, 12 May 2013 23:16:55 +0100 Subject: [PATCH 103/130] files < 16bytes, upload/download bug fix, see pull #45 --- mega/mega.py | 16 ++++++++++++++-- setup.py | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 2a3f0d1..eb808ed 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -457,7 +457,13 @@ def download_file(self, file_handle, file_key, dest_path=None, dest_filename=Non for i in range(0, len(chunk)-16, 16): block = chunk[i:i + 16] encryptor.encrypt(block) - i += 16 + + #fix for files under 16 bytes failing + if file_size > 16: + i += 16 + else: + i = 0 + block = chunk[i:i + 16] if len(block) % 16: block += '\0' * (16 - (len(block) % 16)) @@ -514,7 +520,13 @@ def upload(self, filename, dest=None, dest_filename=None): for i in range(0, len(chunk)-16, 16): block = chunk[i:i + 16] encryptor.encrypt(block) - i += 16 + + #fix for files under 16 bytes failing + if file_size > 16: + i += 16 + else: + i = 0 + block = chunk[i:i + 16] if len(block) % 16: block += '\0' * (16 - len(block) % 16) diff --git a/setup.py b/setup.py index ef198bb..69772b8 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.13', + version='0.9.14', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 50ebd4f1c149e582e3ccc089b0a83805a02f4bf7 Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 16 May 2013 00:44:32 +0100 Subject: [PATCH 104/130] basic unit testing --- tests.py => examples.py | 4 +- mega/mega.py | 2 +- setup.py | 2 +- tests/unit-tests.py | 123 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 4 deletions(-) rename tests.py => examples.py (94%) create mode 100644 tests/unit-tests.py diff --git a/tests.py b/examples.py similarity index 94% rename from tests.py rename to examples.py index 12897a3..dd9c43f 100644 --- a/tests.py +++ b/examples.py @@ -35,10 +35,10 @@ def test(): print(files[file]) # upload file - print(m.upload('tests.py')) + print(m.upload('examples.py')) # search for a file in account - file = m.find('tests.py') + file = m.find('examples.py') if file: # get public link diff --git a/mega/mega.py b/mega/mega.py index eb808ed..49b6593 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -84,7 +84,7 @@ def api_request(self, data): data = [data] req = requests.post( - '{0}://g.api.{1}/cs'.format(self.schema, self.domain), + '{0}://eu.api.{1}/cs'.format(self.schema, self.domain), params=params, data=json.dumps(data), timeout=self.timeout) diff --git a/setup.py b/setup.py index 69772b8..2b2ba4b 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.14', + version='0.9.15', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', diff --git a/tests/unit-tests.py b/tests/unit-tests.py new file mode 100644 index 0000000..1db1b44 --- /dev/null +++ b/tests/unit-tests.py @@ -0,0 +1,123 @@ +""" +These unit tests will upload a test file,a test folder and a test contact, +Perform api operations on them, +And them remove them from your account. +""" +from mega import Mega +import unittest +import random +import os + +email = 'your@email.com' +password = 'password' + +mega = Mega() +m = mega.login(email, password) + +FIND_RESP = None +TEST_CONTACT = 'test@mega.co.nz' +TEST_PUBLIC_URL = 'https://mega.co.nz/#!EYI2VagT!Ic1yblki8oM4v6XHquCe4gu84kxc4glFchj8OvcT5lw' +TEST_FILE = os.path.basename(__file__) +TEST_FOLDER = 'mega.py_testfolder_{0}'.format(random.random()) + + +def pre_test(): + m.upload(TEST_FILE) + # cached response to lower API requests for testing + FIND_RESP = m.find(TEST_FILE) + if FIND_RESP: + return True + else: + raise ValueError('Pre-test functions failed!') + + +class TestMega(unittest.TestCase): + + def test_mega(self): + self.assertTrue(isinstance(mega, Mega)) + + def test_login(self): + self.assertTrue(isinstance(mega, Mega)) + + def test_get_user(self): + resp = m.get_user() + self.assertTrue(isinstance(resp, dict)) + + def test_get_quota(self): + resp = m.get_quota() + self.assertTrue(isinstance(resp, long)) + + def test_get_storage_space(self): + resp = m.get_storage_space(mega=True) + self.assertTrue(isinstance(resp, dict)) + + def test_get_files(self): + files = m.get_files() + self.assertTrue(isinstance(files, dict)) + + def test_find(self): + file = FIND_RESP + if file: + self.assertTrue(isinstance(file, tuple), msg='this is {0} end'.format(file)) + + def test_get_link(self): + file = FIND_RESP + if file: + link = m.get_link(file) + self.assertTrue(isinstance(link, str)) + + def test_import_public_url(self): + resp = m.import_public_url(TEST_PUBLIC_URL) + file_handle = m.get_id_from_obj(resp) + resp = m.destroy(file_handle) + self.assertTrue(isinstance(resp, int)) + + def test_create_folder(self): + resp = m.create_folder(TEST_FOLDER) + self.assertTrue(isinstance(resp, dict)) + + def test_rename(self): + file = m.find(TEST_FOLDER) + if file: + resp = m.rename(file, TEST_FOLDER) + self.assertTrue(isinstance(resp, int)) + + def test_delete_folder(self): + folder_node = m.find(TEST_FOLDER)[0] + resp = m.delete(folder_node) + self.assertTrue(isinstance(resp, int)) + + def test_delete(self): + file = m.find(TEST_FILE) + if file: + resp = m.delete(file[0]) + self.assertTrue(isinstance(resp, int)) + else: + raise ValueError('file not found') + + def test_destroy(self): + file = m.find(TEST_FILE) + if file: + resp = m.destroy(file[0]) + self.assertTrue(isinstance(resp, int)) + else: + raise ValueError('file not found') + + def test_empty_trash(self): + #resp None if already empty, else int + resp = m.empty_trash() + if resp is not None: + self.assertTrue(isinstance(resp, int)) + + def test_add_contact(self): + resp = m.add_contact(TEST_CONTACT) + self.assertTrue(isinstance(resp, int)) + + def test_remove_contact(self): + resp = m.remove_contact(TEST_CONTACT) + self.assertTrue(isinstance(resp, int)) + + +if __name__ == '__main__': + if pre_test(): + unittest.main() From 58aed39d063325e93df83ef636ed07330fa71741 Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 16 May 2013 00:46:47 +0100 Subject: [PATCH 105/130] basic unit testing --- tests/unit-tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit-tests.py b/tests/unit-tests.py index 1db1b44..dfabee3 100644 --- a/tests/unit-tests.py +++ b/tests/unit-tests.py @@ -58,7 +58,7 @@ def test_get_files(self): def test_find(self): file = FIND_RESP if file: - self.assertTrue(isinstance(file, tuple), msg='this is {0} end'.format(file)) + self.assertTrue(isinstance(file, tuple)) def test_get_link(self): file = FIND_RESP From 42f7736bbf5b63d41c06adcb5ce8d3cf859ad37b Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 16 May 2013 01:07:14 +0100 Subject: [PATCH 106/130] basic unit testing --- mega/mega.py | 2 +- tests/unit-tests.py | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 49b6593..eb808ed 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -84,7 +84,7 @@ def api_request(self, data): data = [data] req = requests.post( - '{0}://eu.api.{1}/cs'.format(self.schema, self.domain), + '{0}://g.api.{1}/cs'.format(self.schema, self.domain), params=params, data=json.dumps(data), timeout=self.timeout) diff --git a/tests/unit-tests.py b/tests/unit-tests.py index dfabee3..f101f7b 100644 --- a/tests/unit-tests.py +++ b/tests/unit-tests.py @@ -22,12 +22,11 @@ def pre_test(): - m.upload(TEST_FILE) - # cached response to lower API requests for testing - FIND_RESP = m.find(TEST_FILE) - if FIND_RESP: + global FIND_RESP + try: + m.upload(TEST_FILE) return True - else: + except: raise ValueError('Pre-test functions failed!') @@ -55,13 +54,8 @@ def test_get_files(self): files = m.get_files() self.assertTrue(isinstance(files, dict)) - def test_find(self): - file = FIND_RESP - if file: - self.assertTrue(isinstance(file, tuple)) - def test_get_link(self): - file = FIND_RESP + file = m.find(TEST_FILE) if file: link = m.get_link(file) self.assertTrue(isinstance(link, str)) From 490941cd127d007560f942d731313281aa6a4fc1 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 00:49:32 +0100 Subject: [PATCH 107/130] travis --- requirements.txt | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c48d0a3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests>=0.10 +pycrypto diff --git a/setup.py b/setup.py index 2b2ba4b..bb43279 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.15', + version='0.9.16', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', From 3c94fa922d69ffe287cbf7dfdf585c0020e5d53b Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 00:53:52 +0100 Subject: [PATCH 108/130] travis --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 21af51f..559a351 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Mega.py +[![Build Status](https://travis-ci.org/richardasaurus/mega.py.png?branch=master)](https://travis-ci.org/richardasaurus/mega.py) Python library for the Mega.co.nz API, currently supporting: - login From fd42769f962c764043518b6df89e7b5d09c612a7 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 00:54:55 +0100 Subject: [PATCH 109/130] ignore update --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1a4e3fd..c85d7ae 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ # Hidden files # ################### -.* # Build files # ################### build/ From ac4bf5c9b08f90ef98d299cdb3e934b5914d5c91 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 00:55:10 +0100 Subject: [PATCH 110/130] travis --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bebb40f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: python +python: + - "2.6" + - "2.7" +install: pip install -r requirements.txt --use-mirrors +script: python tests/unit-tests.py test \ No newline at end of file From 75854fc434f08c26505ed309271b54859711cde0 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 01:01:30 +0100 Subject: [PATCH 111/130] travis --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 559a351..6be5fbd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Mega.py [![Build Status](https://travis-ci.org/richardasaurus/mega.py.png?branch=master)](https://travis-ci.org/richardasaurus/mega.py) +[https://pypip.in/d/mega.py/badge.png](https://crate.io/packages/mega.py/) Python library for the Mega.co.nz API, currently supporting: - login From 1991d8cd39bcf35b94b20178bf6cd79041b8a5b4 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 01:02:09 +0100 Subject: [PATCH 112/130] travis --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6be5fbd..dc95d66 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mega.py [![Build Status](https://travis-ci.org/richardasaurus/mega.py.png?branch=master)](https://travis-ci.org/richardasaurus/mega.py) -[https://pypip.in/d/mega.py/badge.png](https://crate.io/packages/mega.py/) +[![Downloads](https://pypip.in/d/mega.py/badge.png)](https://crate.io/packages/mega.py/) Python library for the Mega.co.nz API, currently supporting: - login From 198007b7f25aea8eff066f1ea1af31bd2f8264bc Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 01:05:43 +0100 Subject: [PATCH 113/130] reqs --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c48d0a3..bc2c6d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests>=0.10 pycrypto +mega.py>=0.9.16 From 0ec846e02bed5e197ae6b563f5740069308f8ea7 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 01:23:25 +0100 Subject: [PATCH 114/130] travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bebb40f..dff8a17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ python: - "2.6" - "2.7" install: pip install -r requirements.txt --use-mirrors -script: python tests/unit-tests.py test \ No newline at end of file +script: python tests/unit-tests.py \ No newline at end of file From 6e2dc604e999d3aaafcce26e6262a37e5e5aa766 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 01:43:31 +0100 Subject: [PATCH 115/130] travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dff8a17..54d8bb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,5 @@ language: python python: - "2.6" - "2.7" -install: pip install -r requirements.txt --use-mirrors +install: pip install -r requirements.txt script: python tests/unit-tests.py \ No newline at end of file From fe7a515241b42127ad6aa8ef10068c8332c6f321 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 01:58:41 +0100 Subject: [PATCH 116/130] anonymous login --- README.md | 2 ++ mega/mega.py | 25 +++++++++++++++++++++++-- requirements.txt | 2 +- setup.py | 2 +- tests/unit-tests.py | 9 ++++----- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index dc95d66..0243168 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ mega = Mega({'verbose': True}) ### Login to Mega ```python m = mega.login(email, password) +# login using a temporary anonymous account +m = mega.login() ``` ### Get user details ```python diff --git a/mega/mega.py b/mega/mega.py index eb808ed..9161dd6 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -27,8 +27,11 @@ def __init__(self, options=None): options = {} self.options = options - def login(self, email, password): - self.login_user(email, password) + def login(self, email=None, password=None): + if email: + self.login_user(email, password) + else: + self.login_anonymous() return self def login_user(self, email, password): @@ -40,6 +43,24 @@ def login_user(self, email, password): raise RequestError(resp) self._login_process(resp, password_aes) + def login_anonymous(self): + master_key = [random.randint(0, 0xFFFFFFFF)] * 4 + password_key = [random.randint(0, 0xFFFFFFFF)] * 4 + session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4 + + user = self.api_request({ + 'a': 'up', + '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}) + #if numeric error code response + if isinstance(resp, int): + raise RequestError(resp) + self._login_process(resp, password_key) + def _login_process(self, resp, password): encrypted_master_key = base64_to_a32(resp['k']) self.master_key = decrypt_key(encrypted_master_key, password) diff --git a/requirements.txt b/requirements.txt index bc2c6d7..26edc79 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests>=0.10 pycrypto -mega.py>=0.9.16 +mega.py diff --git a/setup.py b/setup.py index bb43279..205ce0e 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_package_data(package): setup( name='mega.py', - version='0.9.16', + version='0.9.17', packages=get_packages('mega'), package_data=get_package_data('mega'), description='Python lib for the Mega.co.nz API', diff --git a/tests/unit-tests.py b/tests/unit-tests.py index f101f7b..900cc29 100644 --- a/tests/unit-tests.py +++ b/tests/unit-tests.py @@ -12,7 +12,10 @@ password = 'password' mega = Mega() -m = mega.login(email, password) +# anonymous login +m = mega.login() +# normal login +#m = mega.login(email, password) FIND_RESP = None TEST_CONTACT = 'test@mega.co.nz' @@ -86,16 +89,12 @@ def test_delete(self): if file: resp = m.delete(file[0]) self.assertTrue(isinstance(resp, int)) - else: - raise ValueError('file not found') def test_destroy(self): file = m.find(TEST_FILE) if file: resp = m.destroy(file[0]) self.assertTrue(isinstance(resp, int)) - else: - raise ValueError('file not found') def test_empty_trash(self): #resp None if already empty, else int From 2b8f56af84d8f3fa7db0822df59bc293b6857bfe Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 02:12:46 +0100 Subject: [PATCH 117/130] removed pre test func --- tests/unit-tests.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/unit-tests.py b/tests/unit-tests.py index 900cc29..a2ed9bf 100644 --- a/tests/unit-tests.py +++ b/tests/unit-tests.py @@ -24,15 +24,6 @@ TEST_FOLDER = 'mega.py_testfolder_{0}'.format(random.random()) -def pre_test(): - global FIND_RESP - try: - m.upload(TEST_FILE) - return True - except: - raise ValueError('Pre-test functions failed!') - - class TestMega(unittest.TestCase): def test_mega(self): @@ -112,5 +103,4 @@ def test_remove_contact(self): if __name__ == '__main__': - if pre_test(): - unittest.main() + unittest.main() From 55edbdb6f6c718c783c9c78c4bea2ff50188ec24 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 02:21:03 +0100 Subject: [PATCH 118/130] improved tests --- tests/unit-tests.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/unit-tests.py b/tests/unit-tests.py index a2ed9bf..4582b31 100644 --- a/tests/unit-tests.py +++ b/tests/unit-tests.py @@ -27,79 +27,79 @@ class TestMega(unittest.TestCase): def test_mega(self): - self.assertTrue(isinstance(mega, Mega)) + self.assertIsInstance(mega, Mega) def test_login(self): - self.assertTrue(isinstance(mega, Mega)) + self.assertIsInstance(mega, Mega) def test_get_user(self): resp = m.get_user() - self.assertTrue(isinstance(resp, dict)) + self.assertIsInstance(resp, dict) def test_get_quota(self): resp = m.get_quota() - self.assertTrue(isinstance(resp, long)) + self.assertIsInstance(resp, long) def test_get_storage_space(self): resp = m.get_storage_space(mega=True) - self.assertTrue(isinstance(resp, dict)) + self.assertIsInstance(resp, dict) def test_get_files(self): files = m.get_files() - self.assertTrue(isinstance(files, dict)) + self.assertIsInstance(files, dict) def test_get_link(self): file = m.find(TEST_FILE) if file: link = m.get_link(file) - self.assertTrue(isinstance(link, str)) + self.assertIsInstance(link, str) def test_import_public_url(self): resp = m.import_public_url(TEST_PUBLIC_URL) file_handle = m.get_id_from_obj(resp) resp = m.destroy(file_handle) - self.assertTrue(isinstance(resp, int)) + self.assertIsInstance(resp, int) def test_create_folder(self): resp = m.create_folder(TEST_FOLDER) - self.assertTrue(isinstance(resp, dict)) + self.assertIsInstance(resp, dict) def test_rename(self): file = m.find(TEST_FOLDER) if file: resp = m.rename(file, TEST_FOLDER) - self.assertTrue(isinstance(resp, int)) + self.assertIsInstance(resp, int) def test_delete_folder(self): folder_node = m.find(TEST_FOLDER)[0] resp = m.delete(folder_node) - self.assertTrue(isinstance(resp, int)) + self.assertIsInstance(resp, int) def test_delete(self): file = m.find(TEST_FILE) if file: resp = m.delete(file[0]) - self.assertTrue(isinstance(resp, int)) + self.assertIsInstance(resp, int) def test_destroy(self): file = m.find(TEST_FILE) if file: resp = m.destroy(file[0]) - self.assertTrue(isinstance(resp, int)) + self.assertIsInstance(resp, int) def test_empty_trash(self): #resp None if already empty, else int resp = m.empty_trash() if resp is not None: - self.assertTrue(isinstance(resp, int)) + self.assertIsInstance(resp, int) def test_add_contact(self): resp = m.add_contact(TEST_CONTACT) - self.assertTrue(isinstance(resp, int)) + self.assertIsInstance(resp, int) def test_remove_contact(self): resp = m.remove_contact(TEST_CONTACT) - self.assertTrue(isinstance(resp, int)) + self.assertIsInstance(resp, int) if __name__ == '__main__': From 245fee26ae1ba5a42eacb7a82a14f5580c45290f Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 02:43:48 +0100 Subject: [PATCH 119/130] improved tests --- tests/unit-tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit-tests.py b/tests/unit-tests.py index 4582b31..904cfe6 100644 --- a/tests/unit-tests.py +++ b/tests/unit-tests.py @@ -38,7 +38,7 @@ def test_get_user(self): def test_get_quota(self): resp = m.get_quota() - self.assertIsInstance(resp, long) + self.assertIsInstance(int(resp), int) def test_get_storage_space(self): resp = m.get_storage_space(mega=True) From 40d8620ed4d65cab45ee5bab31e4e6f21f2498a4 Mon Sep 17 00:00:00 2001 From: richard Date: Sat, 18 May 2013 02:45:50 +0100 Subject: [PATCH 120/130] travis --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 54d8bb2..616c4d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "2.6" - "2.7" install: pip install -r requirements.txt script: python tests/unit-tests.py \ No newline at end of file From 752e2e88702113092350393d8a7a19f83523ee7f Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 23 May 2013 23:48:48 +0100 Subject: [PATCH 121/130] api info file started --- API_INFO.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 API_INFO.md diff --git a/API_INFO.md b/API_INFO.md new file mode 100644 index 0000000..cc575cd --- /dev/null +++ b/API_INFO.md @@ -0,0 +1,53 @@ +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): + From d20dcbdc2b1f97d9afdfef165c15ac1ab7bb0845 Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 23 May 2013 23:50:06 +0100 Subject: [PATCH 122/130] api info file started --- API_INFO.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/API_INFO.md b/API_INFO.md index cc575cd..de1c523 100644 --- a/API_INFO.md +++ b/API_INFO.md @@ -7,12 +7,19 @@ This file contains definitions for some of the properties within the API. The ai ### 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 From e6c38d97ed5a0fbc7482630a0774ca5d6ace8c2e Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 23 May 2013 23:51:57 +0100 Subject: [PATCH 123/130] api info file started --- API_INFO.md | 71 ++++++++++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/API_INFO.md b/API_INFO.md index de1c523..dfdca33 100644 --- a/API_INFO.md +++ b/API_INFO.md @@ -6,55 +6,48 @@ This file contains definitions for some of the properties within the API. The ai ### 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 +* '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 +* 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): +* EINTERNAL (-1): +* EARGS (-2): +* EAGAIN (-3) +* ERATELIMIT (-4): #### Upload errors: -EFAILED (-5): -ETOOMANY (-6): -ERANGE (-7): -EEXPIRED (-8): +* 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): +* ENOENT (-9): +* ECIRCULAR (-10): +* EACCESS (-11): +* EEXIST (-12): +* EINCOMPLETE (-13): +* EKEY (-14): +* ESID (-15): +* EBLOCKED (-16): +* EOVERQUOTA (-17): +* ETEMPUNAVAIL (-18): From 1b63ef1c9876ba6cbc1886f8ce06930fefbbcd46 Mon Sep 17 00:00:00 2001 From: richard Date: Fri, 24 May 2013 00:07:53 +0100 Subject: [PATCH 124/130] updated readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0243168..faff93a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Python library for the Mega.co.nz API, currently supporting: This is a work in progress, further functionality coming shortly. +For more detailed information see API_INFO.md + ## How To Use ### Install mega.py package From 42a20b710d148e2411374f3bfa908e2988462ec2 Mon Sep 17 00:00:00 2001 From: richard Date: Tue, 11 Jun 2013 00:05:52 +0100 Subject: [PATCH 125/130] Added upload to destination e.g to readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index faff93a..8f5faa3 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,12 @@ file = m.upload('myfile.doc') m.get_upload_link(file) # see mega.py for destination and filename options ``` +### 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') From 7c88b34a48d5ca871603a8511c7a090469f8b431 Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 27 Jun 2013 22:25:02 +0100 Subject: [PATCH 126/130] more PEP8 generally --- mega/mega.py | 117 +++++++++++++++++++++++++-------------------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 9161dd6..87568ab 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -7,7 +7,6 @@ import random import binascii import requests -import time import shutil from .errors import ValidationError, RequestError from .crypto import * @@ -29,15 +28,15 @@ def __init__(self, options=None): def login(self, email=None, password=None): if email: - self.login_user(email, password) + self._login_user(email, password) else: self.login_anonymous() return self - def login_user(self, email, password): + def _login_user(self, email, password): password_aes = prepare_key(str_to_a32(password)) uh = stringhash(email, password_aes) - resp = self.api_request({'a': 'us', 'user': email, 'uh': uh}) + resp = self._api_request({'a': 'us', 'user': email, 'uh': uh}) #if numeric error code response if isinstance(resp, int): raise RequestError(resp) @@ -48,14 +47,14 @@ def login_anonymous(self): password_key = [random.randint(0, 0xFFFFFFFF)] * 4 session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4 - user = self.api_request({ - 'a': 'up', - '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))) + user = self._api_request({ + 'a': 'up', + '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 isinstance(resp, int): raise RequestError(resp) @@ -93,7 +92,7 @@ def _login_process(self, resp, password): sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid) self.sid = base64_url_encode(sid[:43]) - def api_request(self, data): + def _api_request(self, data): params = {'id': self.sequence_num} self.sequence_num += 1 @@ -116,16 +115,16 @@ def api_request(self, data): raise RequestError(json_resp) return json_resp[0] - def parse_url(self, url): + def _parse_url(self, url): #parse file id and key from url - if ('!' in url): + if '!' in url: match = re.findall(r'/#!(.*)', url) path = match[0] return path else: raise RequestError('Url key missing') - def process_file(self, file, shared_keys): + def _process_file(self, file, shared_keys): """ Process a file """ @@ -180,7 +179,7 @@ def process_file(self, file, shared_keys): file['a'] = {'n': 'Rubbish Bin'} return file - def init_shared_keys(self, files, shared_keys): + def _init_shared_keys(self, files, shared_keys): """ Init shared key not associated with a user. Seems to happen when a folder is shared, @@ -213,12 +212,12 @@ def get_files(self): """ Get all files in account """ - files = self.api_request({'a': 'f', 'c': 1}) + files = self._api_request({'a': 'f', 'c': 1}) files_dict = {} shared_keys = {} - self.init_shared_keys(files, shared_keys) + self._init_shared_keys(files, shared_keys) for file in files['f']: - processed_file = self.process_file(file, shared_keys) + processed_file = self._process_file(file, shared_keys) #ensure each file has a name before returning if processed_file['a']: files_dict[file['h']] = processed_file @@ -231,7 +230,7 @@ def get_upload_link(self, file): """ if 'f' in file: 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:] decrypted_key = a32_to_base64(decrypt_key(base64_to_a32(file_key), self.master_key)) @@ -249,7 +248,7 @@ def get_link(self, file): """ file = file[1] 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: raise RequestError("Can't get a public link from that file (is this a shared file?)") decrypted_key = a32_to_base64(file['key']) @@ -261,7 +260,7 @@ def get_link(self, file): raise ValidationError('File id and key must be present') def get_user(self): - user_data = self.api_request({'a': 'ug'}) + user_data = self._api_request({'a': 'ug'}) return user_data def get_node_by_type(self, type): @@ -275,7 +274,7 @@ def get_node_by_type(self, type): """ nodes = self.get_files() for node in nodes.items(): - if (node[1]['t'] == type): + if node[1]['t'] == type: return node def get_files_in_node(self, target): @@ -288,19 +287,19 @@ def get_files_in_node(self, target): else: node_id = [target] - files = self.api_request({'a': 'f', 'c': 1}) + files = self._api_request({'a': 'f', 'c': 1}) files_dict = {} shared_keys = {} - self.init_shared_keys(files, shared_keys) + self._init_shared_keys(files, shared_keys) for file in files['f']: - processed_file = self.process_file(file, shared_keys) + processed_file = self._process_file(file, shared_keys) if processed_file['a'] and processed_file['p'] == node_id[0]: files_dict[file['h']] = processed_file return files_dict def get_id_from_public_handle(self, public_handle): #get node data - node_data = self.api_request({'a': 'f', 'f': 1, 'p': public_handle}) + node_data = self._api_request({'a': 'f', 'f': 1, 'p': public_handle}) node_id = self.get_id_from_obj(node_data) return node_id @@ -319,7 +318,7 @@ def get_quota(self): """ 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}) #convert bytes to megabyes return json_resp['mstrg'] / 1048576 @@ -340,7 +339,7 @@ def get_storage_space(self, giga=False, mega=False, kilo=False): unit_coef = 1048576 if giga: unit_coef = 1073741824 - json_resp = self.api_request({'a': 'uq', 'xfer': 1, 'strg': 1}) + json_resp = self._api_request({'a': 'uq', 'xfer': 1, 'strg': 1}) return { 'used': json_resp['cstrg'] / unit_coef, 'total': json_resp['mstrg'] / unit_coef, @@ -350,7 +349,7 @@ def get_balance(self): """ Get account monetary balance, Pro accounts only """ - user_data = self.api_request({"a": "uq", "pro": 1}) + user_data = self._api_request({"a": "uq", "pro": 1}) if 'balance' in user_data: return user_data['balance'] @@ -366,7 +365,7 @@ def delete_url(self, url): """ Delete a file by its url """ - path = self.parse_url(url).split('!') + path = self._parse_url(url).split('!') public_handle = path[0] file_id = self.get_id_from_public_handle(public_handle) return self.move(file_id, 4) @@ -375,7 +374,7 @@ def destroy(self, file_id): """ Destroy a file by its private id """ - return self.api_request({'a': 'd', + return self._api_request({'a': 'd', 'n': file_id, 'i': self.request_id}) @@ -383,7 +382,7 @@ def destroy_url(self, url): """ Destroy a file by its url """ - path = self.parse_url(url).split('!') + path = self._parse_url(url).split('!') public_handle = path[0] file_id = self.get_id_from_public_handle(public_handle) return self.destroy(file_id) @@ -399,7 +398,7 @@ def empty_trash(self): post_list.append({"a": "d", "n": file, "i": self.request_id}) - return self.api_request(post_list) + return self._api_request(post_list) ########################################################################## # DOWNLOAD @@ -407,31 +406,31 @@ def download(self, file, dest_path=None, dest_filename=None): """ 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): """ Download a file by it's public url """ - path = self.parse_url(url).split('!') + path = self._parse_url(url).split('!') file_id = path[0] 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): - if file is 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 is_public: 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: - 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], file_key[2] ^ file_key[6], file_key[3] ^ file_key[7]) iv = file_key[4:6] + (0, 0) meta_mac = file_key[6:8] else: - file_data = self.api_request({'a': 'g', 'g': 1, 'n': file['h']}) + file_data = self._api_request({'a': 'g', 'g': 1, 'n': file['h']}) k = file['k'] iv = file['iv'] meta_mac = file['meta_mac'] @@ -518,7 +517,7 @@ def upload(self, filename, dest=None, dest_filename=None): #request upload url, call 'u' method input_file = open(filename, 'rb') file_size = os.path.getsize(filename) - ul_url = self.api_request({'a': 'u', 's': file_size})['p'] + ul_url = self._api_request({'a': 'u', 's': file_size})['p'] #generate random aes key (128) for file ul_key = [random.randint(0, 0xFFFFFFFF) for _ in range(6)] @@ -579,11 +578,11 @@ def upload(self, filename, dest=None, dest_filename=None): ul_key[4], ul_key[5], meta_mac[0], meta_mac[1]] encrypted_key = a32_to_base64(encrypt_key(key, self.master_key)) #update attributes - data = self.api_request({'a': 'p', 't': dest, 'n': [{ - 'h': completion_file_handle, - 't': 0, - 'a': encrypt_attribs, - 'k': encrypted_key}]}) + data = self._api_request({'a': 'p', 't': dest, 'n': [{ + 'h': completion_file_handle, + 't': 0, + 'a': encrypt_attribs, + 'k': encrypted_key}]}) #close input file and return API msg input_file.close() return data @@ -607,7 +606,7 @@ def create_folder(self, name, dest=None): encrypted_key = a32_to_base64(encrypt_key(ul_key[:4], self.master_key)) #update attributes - data = self.api_request({'a': 'p', + data = self._api_request({'a': 'p', 't': dest, 'n': [{ 'h': 'xxxxxxxx', @@ -628,7 +627,7 @@ def rename(self, file, new_name): encrypted_key = a32_to_base64(encrypt_key(file['key'], self.master_key)) #update attributes - data = self.api_request([{ + data = self._api_request([{ 'a': 'a', 'attr': encrypt_attribs, 'key': encrypted_key, @@ -666,7 +665,7 @@ def move(self, file_id, target): else: file = target[1] target_node_id = file['h'] - return self.api_request({'a': 'm', + return self._api_request({'a': 'm', 'n': file_id, 't': target_node_id, 'i': self.request_id}) @@ -697,10 +696,10 @@ def _edit_contact(self, email, add): if not re.match(r"[^@]+@[^@]+\.[^@]+", email): ValidationError('add_contact requires a valid email address') else: - return self.api_request({'a': 'ur', - 'u': email, - 'l': l, - 'i': self.request_id}) + return self._api_request({'a': 'ur', + 'u': email, + 'l': l, + 'i': self.request_id}) def get_contacts(self): raise NotImplementedError() @@ -722,21 +721,21 @@ def get_public_url_info(self, url): """ Get size and name from a public url, dict returned """ - file_handle, file_key = self.parse_url(url).split('!') + file_handle, file_key = self._parse_url(url).split('!') return self.get_public_file_info(file_handle, file_key) def import_public_url(self, url, dest_node=None, dest_name=None): """ 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) def get_public_file_info(self, file_handle, file_key): """ Get size and name of a public file """ - data = self.api_request({ + data = self._api_request({ 'a': 'g', 'p': file_handle, 'ssm': 1}) @@ -753,7 +752,7 @@ def get_public_file_info(self, file_handle, file_key): size = data['s'] unencrypted_attrs = decrypt_attr(base64_url_decode(data['at']), k) - if not(unencrypted_attrs): + if not unencrypted_attrs: return None result = { @@ -783,7 +782,7 @@ def import_public_file(self, file_handle, file_key, dest_node=None, dest_name=No encrypted_key = a32_to_base64(encrypt_key(key, self.master_key)) 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'], 'n': [{ From 9263d0cc063090a597fc33e53615d124304ff505 Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 27 Jun 2013 22:27:58 +0100 Subject: [PATCH 127/130] more PEP8 generally --- mega/crypto.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mega/crypto.py b/mega/crypto.py index c439971..c273c33 100644 --- a/mega/crypto.py +++ b/mega/crypto.py @@ -117,6 +117,7 @@ def get_chunks(size): s += 0x20000 yield(p, size-p) + # more general functions def make_id(length): text = '' From b1ada6ad692d7ea3b7e93d80f4ac0eef7eba9b05 Mon Sep 17 00:00:00 2001 From: asanchez Date: Sat, 9 Nov 2013 18:11:27 +0100 Subject: [PATCH 128/130] Upload supports empty files --- mega/mega.py | 60 ++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/mega/mega.py b/mega/mega.py index 87568ab..1746cf0 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -531,37 +531,41 @@ def upload(self, filename, dest=None, dest_filename=None): mac_str = '\0' * 16 mac_encryptor = AES.new(k_str, AES.MODE_CBC, mac_str) iv_str = a32_to_str([ul_key[4], ul_key[5], ul_key[4], ul_key[5]]) + if file_size > 0: + for chunk_start, chunk_size in get_chunks(file_size): + chunk = input_file.read(chunk_size) + upload_progress += len(chunk) + + encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) + for i in range(0, len(chunk)-16, 16): + block = chunk[i:i + 16] + encryptor.encrypt(block) + + #fix for files under 16 bytes failing + if file_size > 16: + i += 16 + else: + i = 0 - for chunk_start, chunk_size in get_chunks(file_size): - chunk = input_file.read(chunk_size) - upload_progress += len(chunk) - - encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) - for i in range(0, len(chunk)-16, 16): block = chunk[i:i + 16] - encryptor.encrypt(block) - - #fix for files under 16 bytes failing - if file_size > 16: - i += 16 - else: - i = 0 - - block = chunk[i:i + 16] - if len(block) % 16: - block += '\0' * (16 - len(block) % 16) - mac_str = mac_encryptor.encrypt(encryptor.encrypt(block)) - - #encrypt file and upload - chunk = aes.encrypt(chunk) - output_file = requests.post(ul_url + "/" + str(chunk_start), - data=chunk, timeout=self.timeout) + if len(block) % 16: + block += '\0' * (16 - len(block) % 16) + mac_str = mac_encryptor.encrypt(encryptor.encrypt(block)) + + #encrypt file and upload + chunk = aes.encrypt(chunk) + output_file = requests.post(ul_url + "/" + str(chunk_start), + data=chunk, timeout=self.timeout) + completion_file_handle = output_file.text + + if self.options.get('verbose') is True: + # upload progress + print('{0} of {1} uploaded'.format(upload_progress, file_size)) + else: + output_file = requests.post(ul_url + "/0", + data='', timeout=self.timeout) completion_file_handle = output_file.text - - if self.options.get('verbose') is True: - # upload progress - print('{0} of {1} uploaded'.format(upload_progress, file_size)) - + file_mac = str_to_a32(mac_str) #determine meta mac From 3bd15a9413194651869883887f47b836558a84b1 Mon Sep 17 00:00:00 2001 From: asanchez Date: Sat, 9 Nov 2013 18:37:32 +0100 Subject: [PATCH 129/130] It is possible to find a folder with a "path" --- mega/mega.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/mega/mega.py b/mega/mega.py index 1746cf0..fa473e9 100644 --- a/mega/mega.py +++ b/mega/mega.py @@ -199,6 +199,34 @@ def _init_shared_keys(self, files, shared_keys): ########################################################################## # GET + + def find_path_descriptor(self, path): + """ + Find descriptor of folder inside a path. i.e.: folder1/folder2/folder3 + Params: + path, string like folder1/folder2/folder3 + Return: + Descriptor (str) of folder3 if exists, None otherwise + """ + paths = path.split('/') + + files = self.get_files() + parent_desc = self.root_id + found = False + for foldername in paths: + if foldername != '': + for file in files.iteritems(): + if file[1]['a'] and file[1]['t'] and \ + file[1]['a']['n'] == foldername: + if parent_desc == file[1]['p']: + parent_desc = file[0] + found = True + if found: + found = False + else: + return None + return parent_desc + def find(self, filename): """ Return file object from given filename From 246d9434e891fdb1151932294f74ba79457f9cca Mon Sep 17 00:00:00 2001 From: rodwyer Date: Sun, 6 Jul 2014 14:24:40 +0100 Subject: [PATCH 130/130] Updated readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 8f5faa3..86191db 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +# Deprecated + +Mega.py is now deprecated, please use the official SDK https://github.com/meganz/sdk2. + +I aim to write a wrapper for the SDK when i have the time to do so. + +------------------------ + + # Mega.py [![Build Status](https://travis-ci.org/richardasaurus/mega.py.png?branch=master)](https://travis-ci.org/richardasaurus/mega.py) [![Downloads](https://pypip.in/d/mega.py/badge.png)](https://crate.io/packages/mega.py/)