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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -0
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +1 -1
  5. data/TODO.md +3 -5
  6. data/bin/rmega-dl +47 -0
  7. data/bin/rmega-up +31 -0
  8. data/lib/rmega.rb +35 -3
  9. data/lib/rmega/api_response.rb +80 -0
  10. data/lib/rmega/cli.rb +121 -0
  11. data/lib/rmega/crypto.rb +20 -0
  12. data/lib/rmega/crypto/aes_cbc.rb +46 -0
  13. data/lib/rmega/crypto/aes_ctr.rb +15 -84
  14. data/lib/rmega/crypto/aes_ecb.rb +25 -0
  15. data/lib/rmega/crypto/rsa.rb +21 -12
  16. data/lib/rmega/errors.rb +3 -51
  17. data/lib/rmega/loggable.rb +0 -3
  18. data/lib/rmega/net.rb +56 -0
  19. data/lib/rmega/nodes/deletable.rb +0 -3
  20. data/lib/rmega/nodes/downloadable.rb +73 -30
  21. data/lib/rmega/nodes/expandable.rb +14 -10
  22. data/lib/rmega/nodes/factory.rb +30 -17
  23. data/lib/rmega/nodes/file.rb +0 -4
  24. data/lib/rmega/nodes/folder.rb +4 -14
  25. data/lib/rmega/nodes/inbox.rb +0 -2
  26. data/lib/rmega/nodes/node.rb +48 -25
  27. data/lib/rmega/nodes/node_key.rb +44 -0
  28. data/lib/rmega/nodes/root.rb +0 -4
  29. data/lib/rmega/nodes/trash.rb +0 -3
  30. data/lib/rmega/nodes/uploadable.rb +42 -33
  31. data/lib/rmega/not_inspectable.rb +10 -0
  32. data/lib/rmega/options.rb +22 -5
  33. data/lib/rmega/pool.rb +18 -7
  34. data/lib/rmega/progress.rb +53 -13
  35. data/lib/rmega/session.rb +125 -52
  36. data/lib/rmega/storage.rb +25 -21
  37. data/lib/rmega/utils.rb +23 -183
  38. data/lib/rmega/version.rb +2 -1
  39. data/rmega.gemspec +3 -5
  40. data/spec/integration/file_download_spec.rb +14 -32
  41. data/spec/integration/file_integrity_spec.rb +41 -0
  42. data/spec/integration/file_upload_spec.rb +11 -57
  43. data/spec/integration/folder_download_spec.rb +17 -0
  44. data/spec/integration/folder_operations_spec.rb +30 -30
  45. data/spec/integration/login_spec.rb +3 -3
  46. data/spec/integration/resume_download_spec.rb +53 -0
  47. data/spec/integration_spec_helper.rb +9 -4
  48. data/spec/rmega/lib/cli_spec.rb +12 -0
  49. data/spec/rmega/lib/session_spec.rb +31 -0
  50. data/spec/rmega/lib/storage_spec.rb +27 -0
  51. data/spec/rmega/lib/utils_spec.rb +16 -78
  52. data/spec/spec_helper.rb +1 -4
  53. metadata +30 -40
  54. data/lib/rmega/crypto/aes.rb +0 -35
  55. data/lib/rmega/crypto/crypto.rb +0 -107
  56. data/lib/rmega/crypto/rsa_mega.js +0 -455
  57. data/spec/rmega/lib/crypto/aes_spec.rb +0 -12
  58. 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: 1de0443e3d865f515533f1110ba81e188b50c82f
4
- data.tar.gz: dcfa1879ea580bba7ed15fb6838084fc2ff3fb28
3
+ metadata.gz: 6a223b72cd508e47946e1420769a7246fbeb0d5d
4
+ data.tar.gz: 2ee6197d9a398cb44adce16f0c19aead89bbc4f2
5
5
  SHA512:
6
- metadata.gz: 0d393bdf468cc8f99f58a742be0e82eb9aab70479450fedb4e63f2f1d72aa15dd205860fbf01bbbac1c23e03a427538d9a84533a6c968b5aabe8b686f7eadf05
7
- data.tar.gz: 905433bb7362720a3cdaa5753aee2f7a7a34f6c4f8ce6654efa4fcd75502176fbc8008b29b4dea61a89784a85644adc0d4761f32fdfa69ff5c0a856396bdacdd
6
+ metadata.gz: c40f1ab57edec730541e3fe060f1f5352724a72fcff7f526ae5813ef45e53dffc08fb3d39d8b6ff393fcc233429d10232efae42fbde19c9b8081e061c0789dfc
7
+ data.tar.gz: 269f261bb5d5d787540e21094c3252dff07edface5a0d899bc9c4c67a6bc9dc52fe830bd75023b456f5cf697872f77f8c0149922f939e2c9df8083ca55ffa78e
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.2
6
+ - 2.2.0
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
- storage.download(public_url, '~/Downloads')
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
- * Handle timeouts in download/upload
4
- * Handle error code -3 response, that is "A temporary congestion or server malfunction prevented your request from being processed.". I noticed that this occurs quite often when creating/deleting a folder node :/
5
- * Sometimes uploaded data is corrupted
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
- require 'active_support/core_ext/string/inflections'
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
@@ -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