rmega 0.1.7 → 0.2.0

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