rmega 0.1.7 → 0.2.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.
- 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
|