rmega 0.1.7 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +6 -0
- data/CHANGELOG.md +16 -0
- data/README.md +1 -1
- data/TODO.md +3 -5
- data/bin/rmega-dl +47 -0
- data/bin/rmega-up +31 -0
- data/lib/rmega.rb +35 -3
- data/lib/rmega/api_response.rb +80 -0
- data/lib/rmega/cli.rb +121 -0
- data/lib/rmega/crypto.rb +20 -0
- data/lib/rmega/crypto/aes_cbc.rb +46 -0
- data/lib/rmega/crypto/aes_ctr.rb +15 -84
- data/lib/rmega/crypto/aes_ecb.rb +25 -0
- data/lib/rmega/crypto/rsa.rb +21 -12
- data/lib/rmega/errors.rb +3 -51
- data/lib/rmega/loggable.rb +0 -3
- data/lib/rmega/net.rb +56 -0
- data/lib/rmega/nodes/deletable.rb +0 -3
- data/lib/rmega/nodes/downloadable.rb +73 -30
- data/lib/rmega/nodes/expandable.rb +14 -10
- data/lib/rmega/nodes/factory.rb +30 -17
- data/lib/rmega/nodes/file.rb +0 -4
- data/lib/rmega/nodes/folder.rb +4 -14
- data/lib/rmega/nodes/inbox.rb +0 -2
- data/lib/rmega/nodes/node.rb +48 -25
- data/lib/rmega/nodes/node_key.rb +44 -0
- data/lib/rmega/nodes/root.rb +0 -4
- data/lib/rmega/nodes/trash.rb +0 -3
- data/lib/rmega/nodes/uploadable.rb +42 -33
- data/lib/rmega/not_inspectable.rb +10 -0
- data/lib/rmega/options.rb +22 -5
- data/lib/rmega/pool.rb +18 -7
- data/lib/rmega/progress.rb +53 -13
- data/lib/rmega/session.rb +125 -52
- data/lib/rmega/storage.rb +25 -21
- data/lib/rmega/utils.rb +23 -183
- data/lib/rmega/version.rb +2 -1
- data/rmega.gemspec +3 -5
- data/spec/integration/file_download_spec.rb +14 -32
- data/spec/integration/file_integrity_spec.rb +41 -0
- data/spec/integration/file_upload_spec.rb +11 -57
- data/spec/integration/folder_download_spec.rb +17 -0
- data/spec/integration/folder_operations_spec.rb +30 -30
- data/spec/integration/login_spec.rb +3 -3
- data/spec/integration/resume_download_spec.rb +53 -0
- data/spec/integration_spec_helper.rb +9 -4
- data/spec/rmega/lib/cli_spec.rb +12 -0
- data/spec/rmega/lib/session_spec.rb +31 -0
- data/spec/rmega/lib/storage_spec.rb +27 -0
- data/spec/rmega/lib/utils_spec.rb +16 -78
- data/spec/spec_helper.rb +1 -4
- metadata +30 -40
- data/lib/rmega/crypto/aes.rb +0 -35
- data/lib/rmega/crypto/crypto.rb +0 -107
- data/lib/rmega/crypto/rsa_mega.js +0 -455
- data/spec/rmega/lib/crypto/aes_spec.rb +0 -12
- data/spec/rmega/lib/crypto/crypto_spec.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a223b72cd508e47946e1420769a7246fbeb0d5d
|
4
|
+
data.tar.gz: 2ee6197d9a398cb44adce16f0c19aead89bbc4f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c40f1ab57edec730541e3fe060f1f5352724a72fcff7f526ae5813ef45e53dffc08fb3d39d8b6ff393fcc233429d10232efae42fbde19c9b8081e061c0789dfc
|
7
|
+
data.tar.gz: 269f261bb5d5d787540e21094c3252dff07edface5a0d899bc9c4c67a6bc9dc52fe830bd75023b456f5cf697872f77f8c0149922f939e2c9df8083ca55ffa78e
|
data/.travis.yml
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
## 0.2.0
|
2
|
+
|
3
|
+
### New Features
|
4
|
+
* resumable downloads
|
5
|
+
* rmega-dl command (with folder download support)
|
6
|
+
* rmega-up command
|
7
|
+
* cbc-mac verification (download)
|
8
|
+
* handle network errors without interrupting downloads/uploads
|
9
|
+
* cache shared keys
|
10
|
+
* `Storage#folders` has been removed (use `Storage#shared` that returns only shared folder nodes)
|
11
|
+
|
12
|
+
### Changes
|
13
|
+
* `RequestError` class was removed
|
14
|
+
* Upload now returns the node `handle`
|
15
|
+
* `Storage#download` moved to `Rmega#download`
|
16
|
+
|
1
17
|
## 0.1.7
|
2
18
|
|
3
19
|
* \#9 Fixed decryption of nodes shared by you to others
|
data/README.md
CHANGED
@@ -80,7 +80,7 @@ folder.download("~/Downloads/my_folder")
|
|
80
80
|
|
81
81
|
# Download a file by url
|
82
82
|
publid_url = 'https://mega.co.nz/#!MAkg2Iab!bc9Y2U6d93IlRRKVYpcC9hLZjS4G278OPdH6nTFPDNQ'
|
83
|
-
|
83
|
+
Rmega.download(public_url, '~/Downloads')
|
84
84
|
```
|
85
85
|
|
86
86
|
### Upload
|
data/TODO.md
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
## TODO
|
2
2
|
|
3
|
-
*
|
4
|
-
|
5
|
-
*
|
6
|
-
* Search for TODO in the project for other minor tasks
|
7
|
-
* Refactor the pool class
|
3
|
+
* Add an option to skip mac verification and calculation (upon downloads)
|
4
|
+
|
5
|
+
* Show this gif (https://i.imgur.com/VVl55wj.gif) in the readme?
|
data/bin/rmega-dl
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rmega'
|
4
|
+
require 'rmega/cli'
|
5
|
+
|
6
|
+
include Rmega::CLI::Helpers
|
7
|
+
|
8
|
+
if ARGV.empty?
|
9
|
+
ARGV << '--help'
|
10
|
+
else
|
11
|
+
cli_options[:url] = ARGV[0]
|
12
|
+
end
|
13
|
+
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage:\n"
|
16
|
+
opts.banner << "\t#{File.basename(__FILE__)} url [options]\n"
|
17
|
+
opts.banner << "Options:"
|
18
|
+
|
19
|
+
opts.on("-o PATH", "--output", "Local destination path") { |path|
|
20
|
+
cli_options[:output] = path
|
21
|
+
}
|
22
|
+
|
23
|
+
apply_opt_parser_options(opts)
|
24
|
+
end.parse!
|
25
|
+
|
26
|
+
rescue_errors_and_inerrupt do
|
27
|
+
urls = [cli_options[:url]]
|
28
|
+
|
29
|
+
unless mega_url?(cli_options[:url])
|
30
|
+
urls = scan_mega_urls(Session.new.http_get_content(cli_options[:url])).uniq
|
31
|
+
raise("Nothing to download") if urls.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
urls.each_with_index do |url, index|
|
35
|
+
node = Rmega::Nodes::Factory.build_from_url(url)
|
36
|
+
|
37
|
+
info = if node.type == :folder
|
38
|
+
stats = node.storage.stats
|
39
|
+
"(#{stats[:files]} file#{'s' if stats[:files] > 1}, #{humanize_bytes(stats[:size])})"
|
40
|
+
end
|
41
|
+
|
42
|
+
puts "[#{index+1}/#{urls.count}] #{node.name} #{info}"
|
43
|
+
|
44
|
+
path = cli_options[:output] || Dir.pwd
|
45
|
+
node.download(path)
|
46
|
+
end
|
47
|
+
end
|
data/bin/rmega-up
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rmega'
|
4
|
+
require 'rmega/cli'
|
5
|
+
|
6
|
+
include Rmega::CLI::Helpers
|
7
|
+
|
8
|
+
if ARGV.empty?
|
9
|
+
ARGV << '--help'
|
10
|
+
else
|
11
|
+
cli_options[:path] = ARGV[0]
|
12
|
+
end
|
13
|
+
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage:\n"
|
16
|
+
opts.banner << "\t#{File.basename(__FILE__)} path [options]\n"
|
17
|
+
opts.banner << "Options:"
|
18
|
+
|
19
|
+
apply_opt_parser_options(opts)
|
20
|
+
end.parse!
|
21
|
+
|
22
|
+
rescue_errors_and_inerrupt do
|
23
|
+
raise("File not found - #{cli_options[:path]}") unless File.exists?(cli_options[:path])
|
24
|
+
|
25
|
+
user = cli_options[:user] || raise("User email is required")
|
26
|
+
pass = cli_options[:pass] ||= cli_prompt_password
|
27
|
+
|
28
|
+
session = Rmega::Session.new.login(user, pass)
|
29
|
+
root = session.storage.root
|
30
|
+
root.upload(cli_options[:path])
|
31
|
+
end
|
data/lib/rmega.rb
CHANGED
@@ -1,11 +1,43 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'logger'
|
4
|
+
require 'uri'
|
5
|
+
require 'net/http'
|
6
|
+
require 'base64'
|
7
|
+
require 'openssl'
|
8
|
+
require 'digest/md5'
|
9
|
+
|
1
10
|
require 'active_support/json'
|
11
|
+
require 'active_support/concern'
|
2
12
|
require 'active_support/core_ext/module/delegation'
|
3
|
-
|
4
|
-
require 'httpclient'
|
5
|
-
require 'execjs'
|
13
|
+
|
6
14
|
require 'rmega/version'
|
15
|
+
require 'rmega/loggable'
|
7
16
|
require 'rmega/options'
|
17
|
+
require 'rmega/not_inspectable'
|
18
|
+
require 'rmega/errors'
|
19
|
+
require 'rmega/api_response'
|
20
|
+
require 'rmega/utils'
|
21
|
+
require 'rmega/net'
|
22
|
+
require 'rmega/pool'
|
23
|
+
require 'rmega/progress'
|
24
|
+
require 'rmega/crypto'
|
8
25
|
require 'rmega/session'
|
26
|
+
require 'rmega/storage'
|
27
|
+
require 'rmega/nodes/factory'
|
28
|
+
|
29
|
+
# Used only in specs
|
30
|
+
require 'yaml'
|
31
|
+
require 'tmpdir'
|
32
|
+
require 'fileutils'
|
9
33
|
|
10
34
|
module Rmega
|
35
|
+
def self.login(email, password)
|
36
|
+
Session.new.login(email, password).storage
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.download(public_url, path = Dir.pwd)
|
40
|
+
node = Nodes::Factory.build_from_url(public_url)
|
41
|
+
return node.download(path)
|
42
|
+
end
|
11
43
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Rmega
|
2
|
+
class APIResponse
|
3
|
+
attr_reader :body, :code
|
4
|
+
|
5
|
+
# Check out the error codes list at https://mega.co.nz/#doc (section 11)
|
6
|
+
ERRORS = {
|
7
|
+
-1 => 'An internal error has occurred. Please submit a bug report, detailing the exact circumstances in which this error occurred.',
|
8
|
+
-2 => 'You have passed invalid arguments to this command.',
|
9
|
+
-3 => 'A temporary congestion or server malfunction prevented your request from being processed. No data was altered. Retry. Retries must be spaced with exponential backoff.',
|
10
|
+
-4 => 'You have exceeded your command weight per time quota. Please wait a few seconds, then try again (this should never happen in sane real-life applications).',
|
11
|
+
-5 => 'The upload failed. Please restart it from scratch.',
|
12
|
+
-6 => 'Too many concurrent IP addresses are accessing this upload target URL.',
|
13
|
+
-7 => 'The upload file packet is out of range or not starting and ending on a chunk boundary.',
|
14
|
+
-8 => 'The upload target URL you are trying to access has expired. Please request a fresh one.',
|
15
|
+
-9 => 'Object (typically, node or user) not found',
|
16
|
+
-10 => 'Circular linkage attempted',
|
17
|
+
-11 => 'Access violation (e.g., trying to write to a read-only share)',
|
18
|
+
-12 => 'Trying to create an object that already exists',
|
19
|
+
-13 => 'Trying to access an incomplete resource',
|
20
|
+
-14 => 'A decryption operation failed (never returned by the API)',
|
21
|
+
-15 => 'Invalid or expired user session, please relogin',
|
22
|
+
-16 => 'User blocked',
|
23
|
+
-17 => 'Request over quota',
|
24
|
+
-18 => 'Resource temporarily not available, please try again later',
|
25
|
+
-19 => 'Too many connections on this resource',
|
26
|
+
-20 => 'Write failed',
|
27
|
+
-21 => 'Read failed',
|
28
|
+
-22 => 'Invalid application key; request not processed',
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
def initialize(http_response)
|
32
|
+
@code = http_response.code.to_i
|
33
|
+
@body = http_response.body ? http_response.body : ""
|
34
|
+
end
|
35
|
+
|
36
|
+
def error?
|
37
|
+
unknown_error? or known_error? or temporary_error?
|
38
|
+
end
|
39
|
+
|
40
|
+
def ok?
|
41
|
+
!error?
|
42
|
+
end
|
43
|
+
|
44
|
+
def as_error
|
45
|
+
if unknown_error?
|
46
|
+
return TemporaryServerError.new
|
47
|
+
elsif temporary_error?
|
48
|
+
return TemporaryServerError.new(error_message)
|
49
|
+
else
|
50
|
+
return ServerError.new(error_message)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def as_json
|
55
|
+
@as_body ||= JSON.parse(body).first
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def as_error_code
|
61
|
+
@error_code ||= body.scan(/\A\[{0,1}(\-\d+)\]{0,1}\z/).flatten.first.to_i
|
62
|
+
end
|
63
|
+
|
64
|
+
def error_message
|
65
|
+
ERRORS[as_error_code]
|
66
|
+
end
|
67
|
+
|
68
|
+
def temporary_error?
|
69
|
+
known_error? and [-3, -6, -18, -19].include?(as_error_code)
|
70
|
+
end
|
71
|
+
|
72
|
+
def unknown_error?
|
73
|
+
code == 500 or body.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
def known_error?
|
77
|
+
as_error_code < 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/rmega/cli.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'io/console'
|
3
|
+
require 'active_support/core_ext/hash'
|
4
|
+
|
5
|
+
module Rmega
|
6
|
+
module CLI
|
7
|
+
module Helpers
|
8
|
+
def cli_options
|
9
|
+
$cli_options ||= {options: {}}
|
10
|
+
end
|
11
|
+
|
12
|
+
def cli_prompt_password
|
13
|
+
print("Enter password: ")
|
14
|
+
password = STDIN.noecho(&:gets)
|
15
|
+
password = password[0..-2] if password.end_with?("\n")
|
16
|
+
puts
|
17
|
+
|
18
|
+
return password
|
19
|
+
end
|
20
|
+
|
21
|
+
def scan_mega_urls(text)
|
22
|
+
text.to_s.scan(Nodes::Factory::URL_REGEXP).flatten.map { |s| "https://mega.co.nz/##{s}" }
|
23
|
+
end
|
24
|
+
|
25
|
+
def mega_url?(url)
|
26
|
+
Nodes::Factory.url?(url)
|
27
|
+
end
|
28
|
+
|
29
|
+
def configuration_filepath
|
30
|
+
File.expand_path('~/.rmega')
|
31
|
+
end
|
32
|
+
|
33
|
+
def write_configuration_file
|
34
|
+
opts = {options: cli_options[:options]}
|
35
|
+
if cli_options[:user]
|
36
|
+
opts[:user] = cli_options[:user]
|
37
|
+
opts[:pass] = cli_options[:pass] || cli_prompt_password
|
38
|
+
end
|
39
|
+
File.open(configuration_filepath, 'wb') { |file| file.write(opts.to_json) }
|
40
|
+
FileUtils.chmod(0600, configuration_filepath)
|
41
|
+
puts "Options saved into #{configuration_filepath}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def read_configuration_file
|
45
|
+
if File.exists?(configuration_filepath)
|
46
|
+
opts = JSON.parse(File.read(configuration_filepath))
|
47
|
+
$cli_options = opts.deep_symbolize_keys.deep_merge(cli_options)
|
48
|
+
puts "Loaded configuration file #{configuration_filepath}" if cli_options[:debug]
|
49
|
+
end
|
50
|
+
rescue Exception => ex
|
51
|
+
raise(ex) if cli_options[:debug]
|
52
|
+
end
|
53
|
+
|
54
|
+
def apply_cli_options
|
55
|
+
Rmega.logger.level = ::Logger::DEBUG if cli_options[:debug]
|
56
|
+
|
57
|
+
cli_options[:options].each do |key, value|
|
58
|
+
Rmega.options.__send__("#{key}=", value)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply_opt_parser_options(opts)
|
63
|
+
opts.on("-t NUM", "--thread_pool_size", "Number of threads to use") { |n|
|
64
|
+
cli_options[:options][:thread_pool_size] = n.to_i
|
65
|
+
}
|
66
|
+
|
67
|
+
opts.on("--proxy-addr ADDRESS", "Http proxy address") { |value|
|
68
|
+
cli_options[:options][:http_proxy_address] = value
|
69
|
+
}
|
70
|
+
|
71
|
+
opts.on("--proxy-port PORT", "Http proxy port") { |value|
|
72
|
+
cli_options[:options][:http_proxy_port] = value.to_i
|
73
|
+
}
|
74
|
+
|
75
|
+
opts.on("-u", "--user USER_EMAIL", "User email address") { |value|
|
76
|
+
cli_options[:user] = value
|
77
|
+
}
|
78
|
+
|
79
|
+
opts.on("--pass [USER_PASSWORD]", "User password (if omitted will prompt for it)") { |value|
|
80
|
+
cli_options[:pass] = value
|
81
|
+
}
|
82
|
+
|
83
|
+
opts.on("--write-cfg", "Write a configuration file with the given options") {
|
84
|
+
cli_options[:write_cfg] = true
|
85
|
+
}
|
86
|
+
|
87
|
+
opts.on("--debug", "Debug mode") {
|
88
|
+
cli_options[:debug] = true
|
89
|
+
}
|
90
|
+
|
91
|
+
opts.on("-v", "--version", "Print the version number") {
|
92
|
+
puts Rmega::VERSION
|
93
|
+
puts Rmega::HOMEPAGE
|
94
|
+
exit
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def humanize_bytes(*args)
|
99
|
+
Progress.humanize_bytes(*args)
|
100
|
+
end
|
101
|
+
|
102
|
+
def rescue_errors_and_inerrupt(&block)
|
103
|
+
if cli_options[:write_cfg]
|
104
|
+
write_configuration_file
|
105
|
+
else
|
106
|
+
read_configuration_file
|
107
|
+
apply_cli_options
|
108
|
+
yield
|
109
|
+
end
|
110
|
+
rescue Interrupt
|
111
|
+
puts "\nInterrupted"
|
112
|
+
rescue Exception => ex
|
113
|
+
if cli_options[:debug]
|
114
|
+
raise(ex)
|
115
|
+
else
|
116
|
+
puts "\nError: #{ex.message}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/rmega/crypto.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rmega/crypto/aes_ecb'
|
2
|
+
require 'rmega/crypto/aes_cbc'
|
3
|
+
require 'rmega/crypto/aes_ctr'
|
4
|
+
require 'rmega/crypto/rsa'
|
5
|
+
|
6
|
+
module Rmega
|
7
|
+
module Crypto
|
8
|
+
include AesCbc
|
9
|
+
include AesEcb
|
10
|
+
include AesCtr
|
11
|
+
include Rsa
|
12
|
+
|
13
|
+
# Check if all the used ciphers are supported
|
14
|
+
ciphers = OpenSSL::Cipher.ciphers.map(&:upcase)
|
15
|
+
%w[AES-128-CBC AES-128-CTR AES-128-ECB].each do |name|
|
16
|
+
next if ciphers.include?(name)
|
17
|
+
warn "WARNING: Your Ruby is compiled with OpenSSL #{OpenSSL::VERSION} and does not support cipher #{name}."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Rmega
|
2
|
+
module Crypto
|
3
|
+
module AesCbc
|
4
|
+
def aes_cbc_cipher
|
5
|
+
OpenSSL::Cipher::AES.new(128, :CBC)
|
6
|
+
end
|
7
|
+
|
8
|
+
def aes_cbc_encrypt(key, data)
|
9
|
+
cipher = aes_cbc_cipher
|
10
|
+
cipher.encrypt
|
11
|
+
cipher.padding = 0
|
12
|
+
cipher.key = key
|
13
|
+
return cipher.update(data) + cipher.final
|
14
|
+
end
|
15
|
+
|
16
|
+
def aes_cbc_decrypt(key, data)
|
17
|
+
cipher = aes_cbc_cipher
|
18
|
+
cipher.decrypt
|
19
|
+
cipher.padding = 0
|
20
|
+
cipher.key = key
|
21
|
+
return cipher.update(data) + cipher.final
|
22
|
+
end
|
23
|
+
|
24
|
+
def aes_cbc_mac(key, data, iv)
|
25
|
+
cipher = aes_cbc_cipher
|
26
|
+
cipher.encrypt
|
27
|
+
cipher.padding = 0
|
28
|
+
cipher.iv = iv if iv
|
29
|
+
cipher.key = key
|
30
|
+
|
31
|
+
n = 0
|
32
|
+
mac = nil
|
33
|
+
|
34
|
+
loop do
|
35
|
+
block = data[n..n+15]
|
36
|
+
break if !block or block.empty?
|
37
|
+
block << "\x0"*(16-block.size) if block.size < 16
|
38
|
+
n += 16
|
39
|
+
mac = cipher.update(block)
|
40
|
+
end
|
41
|
+
|
42
|
+
return mac
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|