rmega 0.0.6 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +108 -55
- data/lib/rmega/crypto/aes.rb +5 -3
- data/lib/rmega/crypto/aes_ctr.rb +5 -2
- data/lib/rmega/crypto/crypto.rb +13 -8
- data/lib/rmega/crypto/rsa.rb +3 -1
- data/lib/rmega/downloader.rb +13 -16
- data/lib/rmega/loggable.rb +9 -2
- data/lib/rmega/nodes/deletable.rb +16 -0
- data/lib/rmega/nodes/expandable.rb +64 -0
- data/lib/rmega/nodes/factory.rb +41 -0
- data/lib/rmega/nodes/file.rb +39 -0
- data/lib/rmega/nodes/folder.rb +30 -0
- data/lib/rmega/nodes/inbox.rb +8 -0
- data/lib/rmega/nodes/node.rb +58 -100
- data/lib/rmega/nodes/root.rb +12 -0
- data/lib/rmega/nodes/trash.rb +18 -0
- data/lib/rmega/nodes/traversable.rb +26 -0
- data/lib/rmega/options.rb +16 -0
- data/lib/rmega/pool.rb +4 -6
- data/lib/rmega/progress.rb +34 -0
- data/lib/rmega/{api_request_error.rb → request_error.rb} +7 -10
- data/lib/rmega/session.rb +20 -31
- data/lib/rmega/storage.rb +19 -68
- data/lib/rmega/uploader.rb +58 -0
- data/lib/rmega/utils.rb +35 -29
- data/lib/rmega/version.rb +1 -1
- data/lib/rmega.rb +8 -53
- data/rmega.gemspec +2 -2
- data/spec/integration/{file_operations_spec.rb → file_download_spec.rb} +4 -8
- data/spec/integration/folder_operations_spec.rb +27 -15
- data/spec/integration/login_spec.rb +6 -5
- data/spec/integration_spec_helper.rb +12 -4
- data/spec/rmega/lib/utils_spec.rb +0 -7
- data/spec/spec_helper.rb +4 -11
- metadata +18 -24
- data/lib/rmega/nodes/file_node.rb +0 -27
- data/lib/rmega/nodes/folder_node.rb +0 -29
data/README.md
CHANGED
@@ -1,94 +1,147 @@
|
|
1
1
|
# Rmega
|
2
2
|
|
3
|
-
A ruby library for
|
4
|
-
|
5
|
-
This work is the result of a reverse engineering of the Mega's Javascript code.
|
3
|
+
A ruby library for MEGA ([https://mega.co.nz/](https://mega.co.nz/))
|
4
|
+
Requirements: Ruby 1.9.3+ and OpenSSL 0.9.8r+
|
6
5
|
|
7
|
-
## Installation
|
8
|
-
|
9
|
-
Rmega is distributed via rubygems, so if you have ruby 1.9.3+ installed
|
10
|
-
system wide, just type `gem install rmega`.
|
11
6
|
|
12
|
-
|
7
|
+
This is the result of a reverse engineering of the MEGA javascript code.
|
8
|
+
This is a work in progress, further functionality are coming.
|
13
9
|
|
14
|
-
$ irb -r rmega
|
15
10
|
|
16
|
-
|
11
|
+
Supported features are:
|
12
|
+
* Login
|
13
|
+
* Searching and browsing
|
14
|
+
* Creating folders
|
15
|
+
* Download of files and folders (multi-thread)
|
16
|
+
* Download with public links (multi-thread)
|
17
|
+
* Upload of files (multi-thread)
|
18
|
+
* Deleting and trashing
|
17
19
|
|
18
|
-
```ruby
|
19
|
-
storage = Rmega.login 'your_email', 'your_password'
|
20
20
|
|
21
|
-
|
22
|
-
nodes = storage.nodes
|
23
|
-
```
|
21
|
+
## Installation
|
24
22
|
|
23
|
+
**Rmega** is distributed via [rubygems.org](https://rubygems.org/).
|
24
|
+
If you have ruby installed system wide, just type `gem install rmega`.
|
25
25
|
|
26
|
-
|
26
|
+
## Usage
|
27
27
|
|
28
28
|
```ruby
|
29
|
-
|
30
|
-
file.name # => "MyDocument1.pdf"
|
31
|
-
file.download '~/Downloads'
|
32
|
-
|
33
|
-
folder = storage.nodes_by_name(/photos/i).first
|
34
|
-
folder.download '~/Downloads/MyAlbums'
|
29
|
+
require 'rmega'
|
35
30
|
```
|
36
31
|
|
37
|
-
|
38
|
-
### Download a file using a public url
|
32
|
+
### Login
|
39
33
|
|
40
34
|
```ruby
|
41
|
-
storage.
|
35
|
+
storage = Rmega.login('your@email.com', 'your_p4ssw0rd')
|
42
36
|
```
|
43
37
|
|
44
|
-
|
45
|
-
### Upload a file
|
38
|
+
### Browsing
|
46
39
|
|
47
40
|
```ruby
|
48
|
-
#
|
49
|
-
storage.
|
50
|
-
|
51
|
-
#
|
52
|
-
|
53
|
-
|
41
|
+
# Print the name of the files in the root folder
|
42
|
+
storage.root.files.each { |file| puts file.name }
|
43
|
+
|
44
|
+
# Print the number of files in each folder
|
45
|
+
storage.root.folders.each do |folder|
|
46
|
+
puts "Folder #{folder.name} contains #{folder.files.size} files."
|
47
|
+
end
|
48
|
+
|
49
|
+
# Print the name and the size of the files in the recyble bin
|
50
|
+
storage.trash.files.each { |file| puts "#{file.name} of #{file.size} bytes" }
|
51
|
+
|
52
|
+
# Print the name of all the files (everywhere)
|
53
|
+
storage.nodes.each do |node|
|
54
|
+
next unless node.type == :file
|
55
|
+
puts node.name
|
56
|
+
end
|
57
|
+
|
58
|
+
# Print all the nodes (files, folders, etc.) within a spefic folder
|
59
|
+
folder = storage.root.folders[12]
|
60
|
+
folder.children.each do |node|
|
61
|
+
puts "Node #{node.name} (#{node.type})"
|
62
|
+
end
|
54
63
|
```
|
55
64
|
|
56
|
-
###
|
65
|
+
### Searching
|
57
66
|
|
58
67
|
```ruby
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
# Gets the public url (the sharable one) of a file
|
63
|
-
my_node.public_url
|
68
|
+
# Search for a file within a specific folder
|
69
|
+
folder = storage.root.folders[2]
|
70
|
+
folder.files.find { |file| file.name == "to_find.txt" }
|
64
71
|
|
65
|
-
#
|
66
|
-
|
72
|
+
# Search for a file everywhere
|
73
|
+
storage.nodes.find { |node| node.type == :file and node.name =~ /my_file/i }
|
67
74
|
|
68
|
-
#
|
69
|
-
parent_folder = storage.nodes_by_name(/photos/i).first
|
70
|
-
folder_node = storage.create_folder parent_folder, "london"
|
75
|
+
# Note: A node can be of type :file, :folder, :root, :inbox and :trash
|
71
76
|
```
|
72
77
|
|
73
|
-
|
78
|
+
### Download
|
74
79
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
80
|
+
```ruby
|
81
|
+
# Download a single file
|
82
|
+
file = storage.root.files.first
|
83
|
+
file.download("~/Downloads")
|
84
|
+
# => Download in progress 15.0MB of 15.0MB (100.0%)
|
85
|
+
|
86
|
+
# Download a folder and all its content recursively
|
87
|
+
folder = storage.nodes.find do |node|
|
88
|
+
node.type == :folder and node.name == 'my_folder'
|
89
|
+
end
|
90
|
+
folder.download("~/Downloads/my_folder")
|
91
|
+
|
92
|
+
# Download a file by url
|
93
|
+
publid_url = 'https://mega.co.nz/#!MAkg2Iab!bc9Y2U6d93IlRRKVYpcC9hLZjS4G278OPdH6nTFPDNQ'
|
94
|
+
storage.download(public_url, '~/Downloads')
|
95
|
+
```
|
79
96
|
|
80
|
-
|
97
|
+
### Upload
|
81
98
|
|
82
|
-
|
99
|
+
```ruby
|
100
|
+
# Upload a file to a specific folder
|
101
|
+
folder = storage.root.folders[3]
|
102
|
+
folder.upload("~/Downloads/my_file.txt")
|
83
103
|
|
84
|
-
|
104
|
+
# Upload a file to the root folder
|
105
|
+
storage.root.upload("~/Downloads/my_other_file.txt")
|
106
|
+
```
|
85
107
|
|
86
|
-
|
108
|
+
### Creating a folder
|
87
109
|
|
88
|
-
|
110
|
+
```ruby
|
111
|
+
# Create a subfolder of the root folder
|
112
|
+
new_folder = storage.root.create_folder("my_documents")
|
113
|
+
|
114
|
+
# Create a subfolder of an existing folder
|
115
|
+
folder = storage.nodes.find do |node|
|
116
|
+
node.type == :folder and node.name == 'my_folder'
|
117
|
+
end
|
118
|
+
folder.create_folder("my_documents")
|
119
|
+
```
|
89
120
|
|
90
|
-
|
121
|
+
### Deleting
|
91
122
|
|
123
|
+
```ruby
|
124
|
+
# Delete a folder
|
125
|
+
folder = storage.root.folders[4]
|
126
|
+
folder.delete
|
127
|
+
|
128
|
+
# Move a folder to the recyle bin
|
129
|
+
folder = storage.root.folders[4]
|
130
|
+
folder.trash
|
131
|
+
|
132
|
+
# Delete a file
|
133
|
+
file = storage.root.folders[3].files.find { |f| f.name =~ /document1/ }
|
134
|
+
file.delete
|
135
|
+
|
136
|
+
# Move a file to the recyle bin
|
137
|
+
file = storage.root.files.last
|
138
|
+
file.trash
|
139
|
+
|
140
|
+
# Empty the trash
|
141
|
+
unless storage.trash.empty?
|
142
|
+
storage.trash.empty!
|
143
|
+
end
|
144
|
+
```
|
92
145
|
|
93
146
|
## Contributing
|
94
147
|
|
data/lib/rmega/crypto/aes.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
1
3
|
module Rmega
|
2
4
|
module Crypto
|
3
5
|
module Aes
|
@@ -8,10 +10,10 @@ module Rmega
|
|
8
10
|
end
|
9
11
|
|
10
12
|
def cipher
|
11
|
-
@cipher ||= OpenSSL::Cipher::AES.new
|
13
|
+
@cipher ||= OpenSSL::Cipher::AES.new(128, :CBC)
|
12
14
|
end
|
13
15
|
|
14
|
-
def encrypt
|
16
|
+
def encrypt(key, data)
|
15
17
|
cipher.reset
|
16
18
|
cipher.padding = 0
|
17
19
|
cipher.encrypt
|
@@ -20,7 +22,7 @@ module Rmega
|
|
20
22
|
result.unpack packing
|
21
23
|
end
|
22
24
|
|
23
|
-
def decrypt
|
25
|
+
def decrypt(key, data)
|
24
26
|
cipher.reset
|
25
27
|
cipher.padding = 0
|
26
28
|
cipher.decrypt
|
data/lib/rmega/crypto/aes_ctr.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
require 'rmega/utils'
|
2
|
+
require 'rmega/crypto/aes'
|
3
|
+
|
1
4
|
module Rmega
|
2
5
|
module Crypto
|
3
6
|
module AesCtr
|
4
7
|
extend self
|
5
8
|
|
6
|
-
def decrypt
|
9
|
+
def decrypt(key, nonce, data)
|
7
10
|
raise "invalid nonce" if nonce.size != 4 or !nonce.respond_to?(:pack)
|
8
11
|
raise "invalid key" if key.size != 4 or !key.respond_to?(:pack)
|
9
12
|
|
@@ -47,7 +50,7 @@ module Rmega
|
|
47
50
|
{data: decrypted_data, mac: mac}
|
48
51
|
end
|
49
52
|
|
50
|
-
def encrypt
|
53
|
+
def encrypt(key, nonce, data)
|
51
54
|
raise "invalid nonce" if nonce.size != 4 or !nonce.respond_to?(:pack)
|
52
55
|
raise "invalid key" if key.size != 4 or !key.respond_to?(:pack)
|
53
56
|
|
data/lib/rmega/crypto/crypto.rb
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
require 'rmega/utils'
|
2
|
+
require 'rmega/crypto/aes'
|
3
|
+
require 'rmega/crypto/aes_ctr'
|
4
|
+
require 'rmega/crypto/rsa'
|
5
|
+
|
1
6
|
module Rmega
|
2
7
|
module Crypto
|
3
8
|
extend self
|
@@ -6,7 +11,7 @@ module Rmega
|
|
6
11
|
Array.new(6).map { rand(0..0xFFFFFFFF) }
|
7
12
|
end
|
8
13
|
|
9
|
-
def prepare_key
|
14
|
+
def prepare_key(ary)
|
10
15
|
pkey = [0x93C467E3,0x7DB0C7A4,0xD1BE3F81,0x0152CB56]
|
11
16
|
65536.times do
|
12
17
|
0.step(ary.size-1, 4) do |j|
|
@@ -20,7 +25,7 @@ module Rmega
|
|
20
25
|
pkey
|
21
26
|
end
|
22
27
|
|
23
|
-
def decrypt_sid
|
28
|
+
def decrypt_sid(key, csid, privk)
|
24
29
|
# if csid ...
|
25
30
|
t = Utils.mpi2b Utils.base64urldecode(csid)
|
26
31
|
privk = Utils.a32_to_str decrypt_key(key, Utils.base64_to_a32(privk))
|
@@ -40,7 +45,7 @@ module Rmega
|
|
40
45
|
Utils.base64urlencode Utils.b2s(decrypted_t)[0..42]
|
41
46
|
end
|
42
47
|
|
43
|
-
def encrypt_attributes
|
48
|
+
def encrypt_attributes(key, attributes_hash)
|
44
49
|
a32key = key.dup
|
45
50
|
if a32key.size > 4
|
46
51
|
a32key = [a32key[0] ^ a32key[4], a32key[1] ^ a32key[5], a32key[2] ^ a32key[6], a32key[3] ^ a32key[7]]
|
@@ -50,7 +55,7 @@ module Rmega
|
|
50
55
|
Crypto::Aes.encrypt a32key, Utils.str_to_a32(attributes_str)
|
51
56
|
end
|
52
57
|
|
53
|
-
def decrypt_attributes
|
58
|
+
def decrypt_attributes(key, attributes_base64)
|
54
59
|
a32key = key.dup
|
55
60
|
if a32key.size > 4
|
56
61
|
a32key = [a32key[0] ^ a32key[4], a32key[1] ^ a32key[5], a32key[2] ^ a32key[6], a32key[3] ^ a32key[7]]
|
@@ -60,11 +65,11 @@ module Rmega
|
|
60
65
|
JSON.parse attributes.gsub(/^MEGA/, '').rstrip
|
61
66
|
end
|
62
67
|
|
63
|
-
def prepare_key_pw
|
68
|
+
def prepare_key_pw(password_str)
|
64
69
|
prepare_key Utils.str_to_a32(password_str)
|
65
70
|
end
|
66
71
|
|
67
|
-
def stringhash
|
72
|
+
def stringhash(aes_key, string)
|
68
73
|
s32 = Utils::str_to_a32 string
|
69
74
|
h32 = [0,0,0,0]
|
70
75
|
|
@@ -74,7 +79,7 @@ module Rmega
|
|
74
79
|
Utils::a32_to_base64 [h32[0],h32[2]]
|
75
80
|
end
|
76
81
|
|
77
|
-
def encrypt_key
|
82
|
+
def encrypt_key(key, data)
|
78
83
|
return Aes.encrypt(key, data) if data.size == 4
|
79
84
|
x = []
|
80
85
|
(0..data.size).step(4) do |i|
|
@@ -85,7 +90,7 @@ module Rmega
|
|
85
90
|
x
|
86
91
|
end
|
87
92
|
|
88
|
-
def decrypt_key
|
93
|
+
def decrypt_key(key, data)
|
89
94
|
return Aes.decrypt(key, data) if data.size == 4
|
90
95
|
x = []
|
91
96
|
(0..data.size).step(4) do |i|
|
data/lib/rmega/crypto/rsa.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'execjs'
|
2
|
+
|
1
3
|
module Rmega
|
2
4
|
module Crypto
|
3
5
|
module Rsa
|
@@ -11,7 +13,7 @@ module Rmega
|
|
11
13
|
@context ||= ExecJS.compile File.read(script_path)
|
12
14
|
end
|
13
15
|
|
14
|
-
def decrypt
|
16
|
+
def decrypt(t, privk)
|
15
17
|
context.call "RSAdecrypt", t, privk[2], privk[0], privk[1], privk[3]
|
16
18
|
end
|
17
19
|
end
|
data/lib/rmega/downloader.rb
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
require 'rmega/loggable'
|
2
|
+
require 'rmega/utils'
|
3
|
+
require 'rmega/pool'
|
4
|
+
require 'rmega/progress'
|
5
|
+
|
1
6
|
module Rmega
|
2
7
|
class Downloader
|
3
8
|
include Loggable
|
@@ -5,7 +10,7 @@ module Rmega
|
|
5
10
|
attr_reader :pool, :base_url, :filesize, :local_path
|
6
11
|
|
7
12
|
def initialize(params)
|
8
|
-
@pool =
|
13
|
+
@pool = Pool.new(params[:threads])
|
9
14
|
@filesize = params[:filesize]
|
10
15
|
@base_url = params[:base_url]
|
11
16
|
@local_path = params[:local_path]
|
@@ -17,7 +22,7 @@ module Rmega
|
|
17
22
|
`dd if=/dev/zero of="#{local_path}" bs=1 count=0 seek=#{filesize} > /dev/null 2>&1`
|
18
23
|
raise "Unable to create file #{local_path}" if File.size(local_path) != filesize
|
19
24
|
|
20
|
-
File.open(local_path, 'r+b').tap { |f| f.rewind }
|
25
|
+
::File.open(local_path, 'r+b').tap { |f| f.rewind }
|
21
26
|
end
|
22
27
|
|
23
28
|
# Downloads a part of the remote file, starting from the start-n byte
|
@@ -25,38 +30,30 @@ module Rmega
|
|
25
30
|
def download_chunk(start, size)
|
26
31
|
stop = start + size - 1
|
27
32
|
url = "#{base_url}/#{start}-#{stop}"
|
28
|
-
# logger.debug "#{Thread.current} downloading chunk @ #{start}"
|
29
33
|
HTTPClient.new.get_content(url)
|
30
34
|
end
|
31
35
|
|
32
36
|
# Writes a buffer in the local file, starting from the start-n byte.
|
33
37
|
def write_chunk(start, buffer)
|
34
|
-
# logger.debug "#{Thread.current} writing chunk @ #{position}"
|
35
38
|
@local_file.seek(start)
|
36
39
|
@local_file.write(buffer)
|
37
40
|
end
|
38
41
|
|
39
|
-
# Shows the progress bar in console
|
40
|
-
def show_progress(increment)
|
41
|
-
Utils.show_progress(:download, filesize, increment)
|
42
|
-
end
|
43
|
-
|
44
42
|
def chunks
|
45
|
-
|
43
|
+
Utils.chunks(filesize)
|
46
44
|
end
|
47
45
|
|
48
|
-
# TODO: checksum check
|
49
46
|
def download(&block)
|
50
47
|
@local_file = allocate
|
51
48
|
|
52
|
-
|
49
|
+
progress = Progress.new(filesize: filesize, verb: 'download')
|
53
50
|
|
54
51
|
chunks.each do |start, size|
|
55
52
|
pool.defer do
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
pool.synchronize { write_chunk(start,
|
53
|
+
encrypted_buffer = download_chunk(start, size)
|
54
|
+
clean_buffer = yield(start, encrypted_buffer)
|
55
|
+
progress.increment(size)
|
56
|
+
pool.synchronize { write_chunk(start, clean_buffer) }
|
60
57
|
end
|
61
58
|
end
|
62
59
|
|
data/lib/rmega/loggable.rb
CHANGED
@@ -1,11 +1,18 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
1
3
|
module Rmega
|
2
4
|
module Loggable
|
3
5
|
def logger
|
4
|
-
|
6
|
+
@@logger ||= begin
|
7
|
+
Logger.new($stdout).tap do |l|
|
8
|
+
l.formatter = Proc.new { | severity, time, progname, msg| "#{msg}\n" }
|
9
|
+
l.level = Logger::ERROR
|
10
|
+
end
|
11
|
+
end
|
5
12
|
end
|
6
13
|
|
7
14
|
def self.included(base)
|
8
|
-
base.send
|
15
|
+
base.send(:extend, self)
|
9
16
|
end
|
10
17
|
end
|
11
18
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rmega/utils'
|
2
|
+
require 'rmega/crypto/crypto'
|
3
|
+
|
4
|
+
module Rmega
|
5
|
+
module Nodes
|
6
|
+
module Deletable
|
7
|
+
def delete
|
8
|
+
request(a: 'd', n: handle)
|
9
|
+
end
|
10
|
+
|
11
|
+
def trash
|
12
|
+
request(a: 'm', n: handle, t: storage.trash.handle)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rmega/utils'
|
2
|
+
require 'rmega/uploader'
|
3
|
+
require 'rmega/crypto/crypto'
|
4
|
+
|
5
|
+
module Rmega
|
6
|
+
module Nodes
|
7
|
+
module Expandable
|
8
|
+
def create_folder(name)
|
9
|
+
key = Crypto.random_key
|
10
|
+
encrypted_attributes = Utils.a32_to_base64 Crypto.encrypt_attributes(key[0..3], {n: name.strip})
|
11
|
+
encrypted_key = Utils.a32_to_base64 Crypto.encrypt_key(session.master_key, key)
|
12
|
+
n = [{h: 'xxxxxxxx', t: 1, a: encrypted_attributes, k: encrypted_key}]
|
13
|
+
data = session.request a: 'p', t: parent_handle, n: n
|
14
|
+
Folder.new(session, data['f'][0])
|
15
|
+
end
|
16
|
+
|
17
|
+
def upload_url(filesize)
|
18
|
+
session.request(a: 'u', s: filesize)['p']
|
19
|
+
end
|
20
|
+
|
21
|
+
def upload(local_path)
|
22
|
+
local_path = ::File.expand_path(local_path)
|
23
|
+
filesize = ::File.size(local_path)
|
24
|
+
|
25
|
+
ul_key = Crypto.random_key
|
26
|
+
aes_key = ul_key[0..3]
|
27
|
+
nonce = ul_key[4..5]
|
28
|
+
|
29
|
+
file_mac = [0, 0, 0, 0]
|
30
|
+
|
31
|
+
uploader = Uploader.new(filesize: filesize, base_url: upload_url(filesize), local_path: local_path)
|
32
|
+
|
33
|
+
uploader.upload do |start, clean_buffer|
|
34
|
+
# TODO: should be (chunk_start/0x1000000000) >>> 0, (chunk_start/0x10) >>> 0
|
35
|
+
nonce = [nonce[0], nonce[1], (start/0x1000000000) >> 0, (start/0x10) >> 0]
|
36
|
+
|
37
|
+
encrypted = Crypto::AesCtr.encrypt(aes_key, nonce, clean_buffer)
|
38
|
+
chunk_mac = encrypted[:mac]
|
39
|
+
|
40
|
+
file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1],
|
41
|
+
file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]]
|
42
|
+
file_mac = Crypto::Aes.encrypt(ul_key[0..3], file_mac)
|
43
|
+
|
44
|
+
encrypted[:data]
|
45
|
+
end
|
46
|
+
|
47
|
+
file_handle = uploader.last_result
|
48
|
+
|
49
|
+
attribs = {n: ::File.basename(local_path)}
|
50
|
+
encrypt_attribs = Utils.a32_to_base64 Crypto.encrypt_attributes(ul_key[0..3], attribs)
|
51
|
+
|
52
|
+
meta_mac = [file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]]
|
53
|
+
|
54
|
+
key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], ul_key[2] ^ meta_mac[0],
|
55
|
+
ul_key[3] ^ meta_mac[1], ul_key[4], ul_key[5], meta_mac[0], meta_mac[1]]
|
56
|
+
|
57
|
+
encrypted_key = Utils.a32_to_base64 Crypto.encrypt_key(session.master_key, key)
|
58
|
+
session.request a: 'p', t: handle, n: [{h: file_handle, t: 0, a: encrypt_attribs, k: encrypted_key}]
|
59
|
+
|
60
|
+
attribs[:n]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rmega/nodes/node'
|
2
|
+
require 'rmega/nodes/file'
|
3
|
+
require 'rmega/nodes/folder'
|
4
|
+
require 'rmega/nodes/inbox'
|
5
|
+
require 'rmega/nodes/root'
|
6
|
+
require 'rmega/nodes/trash'
|
7
|
+
|
8
|
+
module Rmega
|
9
|
+
module Nodes
|
10
|
+
module Factory
|
11
|
+
extend self
|
12
|
+
|
13
|
+
def build(session, data)
|
14
|
+
type_name = type(data['t'])
|
15
|
+
node_class = Nodes.const_get("#{type_name.to_s.capitalize}")
|
16
|
+
node_class.new(session, data)
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO: support other node types than File
|
20
|
+
def build_from_url(session, url)
|
21
|
+
public_handle, key = url.strip.split('!')[1, 2]
|
22
|
+
data = session.request(a: 'g', g: 1, p: public_handle)
|
23
|
+
|
24
|
+
Nodes::File.new(session, data).tap { |n| n.public_url = url }
|
25
|
+
end
|
26
|
+
|
27
|
+
def mega_url?(url)
|
28
|
+
!!(url.to_s =~ /^https:\/\/mega\.co\.nz\/#!.*$/i)
|
29
|
+
end
|
30
|
+
|
31
|
+
def type(number)
|
32
|
+
founded_type = types.find { |k, v| number == v }
|
33
|
+
founded_type.first if founded_type
|
34
|
+
end
|
35
|
+
|
36
|
+
def types
|
37
|
+
{file: 0, folder: 1, root: 2, inbox: 3, trash: 4}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rmega/downloader'
|
2
|
+
require 'rmega/nodes/node'
|
3
|
+
require 'rmega/nodes/deletable'
|
4
|
+
|
5
|
+
module Rmega
|
6
|
+
module Nodes
|
7
|
+
class File < Node
|
8
|
+
include Deletable
|
9
|
+
|
10
|
+
def storage_url
|
11
|
+
@storage_url ||= data['g'] || request(a: 'g', g: 1, n: handle)['g']
|
12
|
+
end
|
13
|
+
|
14
|
+
def size
|
15
|
+
data['s']
|
16
|
+
end
|
17
|
+
|
18
|
+
def download(path)
|
19
|
+
path = ::File.expand_path(path)
|
20
|
+
path = Dir.exists?(path) ? ::File.join(path, name) : path
|
21
|
+
|
22
|
+
logger.info "Download #{name} (#{size} bytes) => #{path}"
|
23
|
+
|
24
|
+
k = decrypted_file_key
|
25
|
+
k = [k[0] ^ k[4], k[1] ^ k[5], k[2] ^ k[6], k[3] ^ k[7]]
|
26
|
+
nonce = decrypted_file_key[4..5]
|
27
|
+
|
28
|
+
donwloader = Downloader.new(base_url: storage_url, filesize: size, local_path: path)
|
29
|
+
|
30
|
+
donwloader.download do |start, buffer|
|
31
|
+
nonce = [nonce[0], nonce[1], (start/0x1000000000) >> 0, (start/0x10) >> 0]
|
32
|
+
Crypto::AesCtr.decrypt(k, nonce, buffer)[:data]
|
33
|
+
end
|
34
|
+
|
35
|
+
path
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rmega/crypto/crypto'
|
2
|
+
require 'rmega/utils'
|
3
|
+
require 'rmega/nodes/node'
|
4
|
+
require 'rmega/nodes/expandable'
|
5
|
+
require 'rmega/nodes/traversable'
|
6
|
+
require 'rmega/nodes/deletable'
|
7
|
+
|
8
|
+
module Rmega
|
9
|
+
module Nodes
|
10
|
+
class Folder < Node
|
11
|
+
include Expandable
|
12
|
+
include Traversable
|
13
|
+
include Deletable
|
14
|
+
|
15
|
+
def download(path)
|
16
|
+
children.each do |node|
|
17
|
+
if node.type == :file
|
18
|
+
node.download path
|
19
|
+
elsif node.type == :folder
|
20
|
+
subfolder = ::File.expand_path ::File.join(path, node.name)
|
21
|
+
Dir.mkdir(subfolder) unless Dir.exists?(subfolder)
|
22
|
+
node.download subfolder
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|