rmega 0.0.6 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|