rmega 0.1.5 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 53ab6c3f8d9135aac13fa5d8556a530d33f0076c
4
- data.tar.gz: e8df74fafe4202607824754b35ffed9558638ef3
3
+ metadata.gz: 461e31696b324da1fdd11d8164b9284f9d0ead32
4
+ data.tar.gz: 6edaa46bdd45cee1447af2bd569bdd812fddb207
5
5
  SHA512:
6
- metadata.gz: 7ca62565f0f2207053354845a74e03bc58ce6612e231b9a70b65d218a25ec79342f709c9513a37f647bcfc61fdb2e1a45b18a42688ff84638e326029af826d49
7
- data.tar.gz: c9082a0e4792a461ef87941969a047413a0593a003ae46338ddfc23a2ac08362fae04f492dd8c217049ed08851040ceea7e08f0cd14422a8131a4435569c2e46
6
+ metadata.gz: 7c38cbd4d4510fa270bf269f57fe3d6fef4730ed421065ffee8d0762ecbd8d47acd2a84311f699193635b7cb6a1bc31c5519778cd1c2f4ac3c9779035ebfe2dd
7
+ data.tar.gz: 1d731406b6c5096f9cb12240ce3af9016b2fd5d728e2594af69156d559c557f7581b0d6c08b566b6a4a98c206bba6dbcd3c1fb40b4c6fcd7c7cd8ddd39a9fd1e
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ ## 0.1.6
2
+
3
+ ### New Features
4
+
5
+ * \#7 Rmega now supports shared folders and files.
6
+
7
+ * The method `Storage#folders` can now be used to get a list of your
8
+ first-level folders + first-level shared folders.
9
+
10
+ * The error codes list is now updated (up to the number -22).
11
+
12
+
13
+ ### Changes
14
+
15
+ * Rmega will now attempt a request up to 10 times (in case of SocketError
16
+ or if the server is temporary busy) before raising the error.
17
+
18
+ * Fixed a race condition that cause corrupted downloads
data/README.md CHANGED
@@ -5,17 +5,6 @@ Requirements: Ruby 1.9.3+ and OpenSSL 0.9.8r+
5
5
 
6
6
 
7
7
  This is the result of a reverse engineering of the MEGA javascript code.
8
- Work in progress, further functionality are coming.
9
-
10
-
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
19
8
 
20
9
 
21
10
  ## Installation
data/TODO.md CHANGED
@@ -1,7 +1,7 @@
1
- ## Rmega todos
1
+ ## TODO
2
2
 
3
3
  * Handle timeouts in download/upload
4
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
5
  * Sometimes uploaded data is corrupted
6
6
  * Search for TODO in the project for other minor tasks
7
- * Resist the hypnotoad's will
7
+ * Refactor the pool class
@@ -10,6 +10,8 @@ module Rmega
10
10
  raise "invalid nonce" if nonce.size != 4 or !nonce.respond_to?(:pack)
11
11
  raise "invalid key" if key.size != 4 or !key.respond_to?(:pack)
12
12
 
13
+ nonce = nonce.dup
14
+
13
15
  mac = [nonce[0], nonce[1], nonce[0], nonce[1]]
14
16
  enc = nil
15
17
  a32 = Utils.str_to_a32 data
@@ -25,12 +25,9 @@ module Rmega
25
25
  pkey
26
26
  end
27
27
 
28
- def decrypt_sid(key, csid, privk)
29
- # if csid ...
30
- t = Utils.mpi2b Utils.base64urldecode(csid)
28
+ def decrypt_rsa_privk(key, privk)
31
29
  privk = Utils.a32_to_str decrypt_key(key, Utils.base64_to_a32(privk))
32
30
  rsa_privk = Array.new 4
33
- # else if tsid (todo)
34
31
 
35
32
  # Decompose private key
36
33
  4.times do |i|
@@ -39,6 +36,13 @@ module Rmega
39
36
  privk = privk[l..-1]
40
37
  end
41
38
 
39
+ rsa_privk
40
+ end
41
+
42
+ def decrypt_sid(rsa_privk, csid)
43
+ # if csid ...
44
+ t = Utils.mpi2b Utils.base64urldecode(csid)
45
+
42
46
  # TODO - remove execjs and build the key using the ruby lib
43
47
  # rsa_key = Crypto::Rsa.build_rsa_key rsa_privk
44
48
  decrypted_t = Rsa.decrypt t, rsa_privk
@@ -0,0 +1,55 @@
1
+ module Rmega
2
+ module Errors
3
+
4
+ # Check out the error codes list at https://mega.co.nz/#doc (section 11)
5
+ CODES = {
6
+ -1 => 'An internal error has occurred. Please submit a bug report, detailing the exact circumstances in which this error occurred.',
7
+ -2 => 'You have passed invalid arguments to this command.',
8
+ -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.',
9
+ -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).',
10
+ -5 => 'The upload failed. Please restart it from scratch.',
11
+ -6 => 'Too many concurrent IP addresses are accessing this upload target URL.',
12
+ -7 => 'The upload file packet is out of range or not starting and ending on a chunk boundary.',
13
+ -8 => 'The upload target URL you are trying to access has expired. Please request a fresh one.',
14
+ -9 => 'Object (typically, node or user) not found',
15
+ -10 => 'Circular linkage attempted',
16
+ -11 => 'Access violation (e.g., trying to write to a read-only share)',
17
+ -12 => 'Trying to create an object that already exists',
18
+ -13 => 'Trying to access an incomplete resource',
19
+ -14 => 'A decryption operation failed (never returned by the API)',
20
+ -15 => 'Invalid or expired user session, please relogin',
21
+ -16 => 'User blocked',
22
+ -17 => 'Request over quota',
23
+ -18 => 'Resource temporarily not available, please try again later',
24
+ -19 => 'Too many connections on this resource',
25
+ -20 => 'Write failed',
26
+ -21 => 'Read failed',
27
+ -22 => 'Invalid application key; request not processed',
28
+ }.freeze
29
+
30
+ class ServerError < StandardError
31
+ attr_reader :code, :message
32
+ attr_accessor :temporary
33
+
34
+ def initialize(value, opts = {})
35
+ if msg = Errors::CODES[value.to_i]
36
+ @code = value.to_i
37
+ @message = msg || "Error code #{@code}"
38
+ else
39
+ @message = value
40
+ end
41
+
42
+ self.temporary = opts[:temporary] || false
43
+
44
+ super(@message)
45
+ end
46
+
47
+ def temporary?
48
+ self.temporary || [-3, -18, -6].include?(@code)
49
+ end
50
+ end
51
+ end
52
+
53
+ # Backward compatibility
54
+ RequestError = Errors::ServerError
55
+ end
@@ -1,18 +1,26 @@
1
1
  require 'logger'
2
+ require 'active_support/concern'
2
3
 
3
4
  module Rmega
5
+ def self.logger
6
+ @logger ||= begin
7
+ logger = Logger.new($stdout)
8
+ logger.level = Logger::ERROR
9
+ logger
10
+ end
11
+ end
12
+
4
13
  module Loggable
14
+ extend ActiveSupport::Concern
15
+
5
16
  def logger
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
17
+ Rmega.logger
12
18
  end
13
19
 
14
- def self.included(base)
15
- base.send(:extend, self)
20
+ module ClassMethods
21
+ def logger
22
+ Rmega.logger
23
+ end
16
24
  end
17
25
  end
18
26
  end
@@ -0,0 +1,71 @@
1
+ require 'rmega/pool'
2
+ require 'rmega/utils'
3
+
4
+ module Rmega
5
+ module Nodes
6
+ module Downloadable
7
+
8
+ # Creates the local file allocating filesize-n bytes (of /dev/zero) for it.
9
+ # Opens the local file to start writing from the beginning of it.
10
+ def allocate(path)
11
+ `dd if=/dev/zero of="#{path}" bs=1 count=0 seek=#{filesize} > /dev/null 2>&1`
12
+ raise "Unable to create file #{path}" if ::File.size(path) != filesize
13
+
14
+ ::File.open(path, 'r+b').tap { |f| f.rewind }
15
+ end
16
+
17
+ # Downloads a part of the remote file, starting from the start-n byte
18
+ # and ending after size-n bytes.
19
+ def download_chunk(start, size)
20
+ stop = start + size - 1
21
+ url = "#{storage_url}/#{start}-#{stop}"
22
+ HTTPClient.new.get_content(url)
23
+ end
24
+
25
+ # Writes a buffer in the local file, starting from the start-n byte.
26
+ def write_chunk(file, start, buffer)
27
+ file.seek(start)
28
+ file.write(buffer)
29
+ end
30
+
31
+ def decrypt_chunk(start, encrypted_buffer)
32
+ k = decrypted_file_key
33
+ nonce = [k[4], k[5], (start/0x1000000000) >> 0, (start/0x10) >> 0]
34
+ decrypt_key = [k[0] ^ k[4], k[1] ^ k[5], k[2] ^ k[6], k[3] ^ k[7]]
35
+ Crypto::AesCtr.decrypt(decrypt_key, nonce, encrypted_buffer)[:data]
36
+ end
37
+
38
+ def download(path)
39
+ path = ::File.expand_path(path)
40
+ path = Dir.exists?(path) ? ::File.join(path, name) : path
41
+
42
+ logger.info "Download #{name} (#{filesize} bytes) => #{path}"
43
+
44
+ pool = Pool.new
45
+ write_mutex = Mutex.new
46
+ file = allocate(path)
47
+
48
+ progress = Progress.new(total: filesize, caption: 'Download')
49
+
50
+ Utils.chunks(filesize).each do |start, size|
51
+ pool.defer do
52
+ encrypted_buffer = download_chunk(start, size)
53
+
54
+ write_mutex.synchronize do
55
+ clean_buffer = decrypt_chunk(start, encrypted_buffer)
56
+ progress.increment(size)
57
+ write_chunk(file, start, clean_buffer)
58
+ end
59
+ end
60
+ end
61
+
62
+ # waits for the last running threads to finish
63
+ pool.wait_done
64
+
65
+ file.flush
66
+ ensure
67
+ file.close rescue nil
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,10 +1,12 @@
1
1
  require 'rmega/utils'
2
- require 'rmega/uploader'
2
+ require 'rmega/nodes/uploadable'
3
3
  require 'rmega/crypto/crypto'
4
4
 
5
5
  module Rmega
6
6
  module Nodes
7
7
  module Expandable
8
+ include Uploadable
9
+
8
10
  def create_folder(name)
9
11
  key = Crypto.random_key
10
12
  encrypted_attributes = Utils.a32_to_base64 Crypto.encrypt_attributes(key[0..3], {n: name.strip})
@@ -17,48 +19,6 @@ module Rmega
17
19
  def upload_url(filesize)
18
20
  session.request(a: 'u', s: filesize)['p']
19
21
  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
22
  end
63
23
  end
64
24
  end
@@ -1,11 +1,12 @@
1
- require 'rmega/downloader'
2
1
  require 'rmega/nodes/node'
3
2
  require 'rmega/nodes/deletable'
3
+ require 'rmega/nodes/downloadable'
4
4
 
5
5
  module Rmega
6
6
  module Nodes
7
7
  class File < Node
8
8
  include Deletable
9
+ include Downloadable
9
10
 
10
11
  def storage_url
11
12
  @storage_url ||= data['g'] || request(a: 'g', g: 1, n: handle)['g']
@@ -15,24 +16,8 @@ module Rmega
15
16
  data['s']
16
17
  end
17
18
 
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
19
+ def filesize
20
+ size
36
21
  end
37
22
  end
38
23
  end
@@ -11,7 +11,7 @@ module Rmega
11
11
 
12
12
  attr_reader :data, :session
13
13
 
14
- delegate :storage, :request, :to => :session
14
+ delegate :storage, :request, :shared_keys, :rsa_privk, :to => :session
15
15
 
16
16
  def initialize(session, data)
17
17
  @session = session
@@ -20,7 +20,7 @@ module Rmega
20
20
 
21
21
  def public_url
22
22
  @public_url ||= begin
23
- b64_dec_key = Utils.a32_to_base64 decrypted_file_key[0..7]
23
+ b64_dec_key = Utils.a32_to_base64(decrypted_file_key[0..7])
24
24
  "https://mega.co.nz/#!#{public_handle}!#{b64_dec_key}"
25
25
  end
26
26
  end
@@ -41,35 +41,65 @@ module Rmega
41
41
  data['p']
42
42
  end
43
43
 
44
- def owner_key
45
- data['k'].split(':').first
44
+ def name
45
+ attributes['n'] if attributes
46
46
  end
47
47
 
48
- def name
49
- return attributes['n'] if attributes
48
+ def file_keys
49
+ return {} unless data['k']
50
+
51
+ pairs = data['k'].split('/')
52
+ pairs.inject({}) do |hash, pair|
53
+ h, k = pair.split(':')
54
+ hash[h] = k
55
+ hash
56
+ end
50
57
  end
51
58
 
52
59
  def file_key
53
- data['k'].split(':').last
60
+ file_keys.values.first
54
61
  end
55
62
 
56
- def decrypted_file_key
57
- if data['k']
58
- Crypto.decrypt_key session.master_key, Utils.base64_to_a32(file_key)
63
+ def shared_root?
64
+ data['su'] && data['sk'] && data['k']
65
+ end
66
+
67
+ def process_shared_key
68
+ h = (shared_keys.keys & file_keys.keys).first
69
+ return [h, shared_keys[h]] if h
70
+
71
+ sk = data['sk']
72
+
73
+ return unless sk
74
+
75
+ shared_key = if sk.size > 22
76
+ sk = Rmega::Utils.mpi2b(Rmega::Utils.base64urldecode(sk))
77
+ dec_sk = Rmega::Crypto::Rsa.decrypt(sk, rsa_privk)
78
+ Utils.str_to_a32(Rmega::Utils.b2s(dec_sk)[0..15])
59
79
  else
60
- Utils.base64_to_a32 public_url.split('!').last
80
+ Crypto.decrypt_key session.master_key, Utils.base64_to_a32(data['sk'])
61
81
  end
82
+
83
+ shared_keys[handle] = shared_key
84
+ [handle, shared_key]
62
85
  end
63
86
 
64
- def can_decrypt_attributes?
65
- !data['u'] or data['u'] == owner_key
87
+ def decrypted_file_key
88
+ h, shared_key = *process_shared_key
89
+
90
+ if shared_key
91
+ Crypto.decrypt_key(shared_key, Utils.base64_to_a32(file_keys[h]))
92
+ elsif file_key
93
+ Crypto.decrypt_key(session.master_key, Utils.base64_to_a32(file_key))
94
+ else
95
+ Utils.base64_to_a32(public_url.split('!').last)
96
+ end
66
97
  end
67
98
 
68
99
  def attributes
69
- @attributes ||= begin
70
- return nil unless can_decrypt_attributes?
71
- Crypto.decrypt_attributes decrypted_file_key, (data['a'] || data['at'])
72
- end
100
+ encrypted = data['a'] || data['at']
101
+ return if !encrypted or encrypted.empty?
102
+ Crypto.decrypt_attributes(decrypted_file_key, encrypted)
73
103
  end
74
104
 
75
105
  def type
@@ -0,0 +1,83 @@
1
+ require 'rmega/utils'
2
+ require 'rmega/pool'
3
+ require 'rmega/progress'
4
+
5
+ module Rmega
6
+ module Nodes
7
+ module Uploadable
8
+ def upload_chunk(base_url, start, buffer)
9
+ size = buffer.length
10
+ stop = start + size - 1
11
+ url = "#{base_url}/#{start}-#{stop}"
12
+
13
+ HTTPClient.new.post(url, buffer).body
14
+ end
15
+
16
+ def read_chunk(file, start, size)
17
+ file.seek(start)
18
+ file.read(size)
19
+ end
20
+
21
+ def encrypt_chunck(rnd_key, file_mac, start, clean_buffer)
22
+ nonce = [rnd_key[4], rnd_key[5], (start/0x1000000000) >> 0, (start/0x10) >> 0]
23
+
24
+ encrypted = Crypto::AesCtr.encrypt(rnd_key[0..3], nonce, clean_buffer)
25
+ chunk_mac, data = encrypted[:mac], encrypted[:data]
26
+
27
+ file_mac = [file_mac[0] ^ chunk_mac[0], file_mac[1] ^ chunk_mac[1],
28
+ file_mac[2] ^ chunk_mac[2], file_mac[3] ^ chunk_mac[3]]
29
+
30
+ file_mac = Crypto::Aes.encrypt(rnd_key[0..3], file_mac)
31
+
32
+ data
33
+ end
34
+
35
+ def upload(path)
36
+ path = ::File.expand_path(path)
37
+ filesize = ::File.size(path)
38
+ file = ::File.open(path, 'rb')
39
+
40
+ ul_key = Crypto.random_key
41
+ file_mac = [0, 0, 0, 0]
42
+ file_handle = nil
43
+ base_url = upload_url(filesize)
44
+
45
+ pool = Pool.new
46
+ read_mutex = Mutex.new
47
+
48
+ progress = Progress.new(total: filesize, caption: 'Upload')
49
+
50
+ Utils.chunks(filesize).each do |start, size|
51
+ pool.defer do
52
+ encrypted_buffer = nil
53
+
54
+ read_mutex.synchronize do
55
+ clean_buffer = read_chunk(file, start, size)
56
+ encrypted_buffer = encrypt_chunck(ul_key, file_mac, start, clean_buffer)
57
+ end
58
+
59
+ file_handle = upload_chunk(base_url, start, encrypted_buffer)
60
+ progress.increment(size)
61
+ end
62
+ end
63
+
64
+ pool.wait_done
65
+
66
+ attribs = {n: ::File.basename(path)}
67
+ encrypt_attribs = Utils.a32_to_base64(Crypto.encrypt_attributes(ul_key[0..3], attribs))
68
+
69
+ meta_mac = [file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]]
70
+
71
+ key = [ul_key[0] ^ ul_key[4], ul_key[1] ^ ul_key[5], ul_key[2] ^ meta_mac[0],
72
+ ul_key[3] ^ meta_mac[1], ul_key[4], ul_key[5], meta_mac[0], meta_mac[1]]
73
+
74
+ encrypted_key = Utils.a32_to_base64 Crypto.encrypt_key(session.master_key, key)
75
+ request(a: 'p', t: handle, n: [{h: file_handle, t: 0, a: encrypt_attribs, k: encrypted_key}])
76
+
77
+ attribs[:n]
78
+ ensure
79
+ file.close
80
+ end
81
+ end
82
+ end
83
+ end
data/lib/rmega/options.rb CHANGED
@@ -4,6 +4,8 @@ module Rmega
4
4
  def self.default_options
5
5
  {
6
6
  upload_timeout: 120,
7
+ max_retries: 10,
8
+ retry_interval: 1,
7
9
  api_request_timeout: 20,
8
10
  api_url: 'https://eu.api.mega.co.nz/cs'
9
11
  }
data/lib/rmega/pool.rb CHANGED
@@ -2,63 +2,58 @@ require 'thread'
2
2
 
3
3
  module Rmega
4
4
  class Pool
5
- MAX = 5
5
+ MAX = 4
6
6
 
7
- def initialize(max)
8
- max ||= MAX
7
+ def initialize(max = MAX)
9
8
  Thread.abort_on_exception = true
9
+
10
10
  @mutex = Mutex.new
11
- @threads = Array.new(max)
12
- end
11
+ @resource = ConditionVariable.new
12
+ @max = max || MAX
13
13
 
14
- # Gets the first position of the pool in which
15
- # a thread could be started.
16
- def available_slot
17
- @threads.each_with_index do |thread, index|
18
- return index if thread.nil? or !thread.alive?
19
- end
20
- nil
14
+ @running = []
15
+ @queue = []
21
16
  end
22
17
 
23
- def synchronize(&block)
24
- @mutex.synchronize(&block)
18
+ def defer(&block)
19
+ synchronize { @queue << block }
20
+ process_queue
25
21
  end
26
22
 
27
- # Returns true if all the threads are finished,
28
- # false otherwise.
29
- def done?
30
- @threads.each { |thread| return false if thread and thread.alive? }
31
- true
23
+ def wait_done
24
+ synchronize { @resource.wait(@mutex) }
32
25
  end
33
26
 
34
- # Blocking. Waits until all the threads are finished.
35
- def wait_done
36
- sleep 0.01 until done?
27
+ private
28
+
29
+ def synchronize(&block)
30
+ @mutex.synchronize(&block)
37
31
  end
38
32
 
39
- # Blocking. Waits until a pool's slot become available and
40
- # returns that position.
41
- # TODO: raise an error on wait timeout.
42
- def wait_available_slot
43
- while true
44
- index = available_slot
45
- return index if index
46
- sleep 0.01
33
+ def process_queue
34
+ synchronize do
35
+ if @running.size < @max
36
+ proc = @queue.shift
37
+ @running << Thread.new(&thread_proc(&proc)) if proc
38
+ end
47
39
  end
48
40
  end
49
41
 
50
- # Sends a KILL signal to all the threads.
51
- def shutdown
52
- @threads.each { |thread| thread.kill if thread.respond_to?(:kill) }
53
- @threads.map! { nil }
42
+ def done?
43
+ synchronize { @queue.empty? && @running.empty? }
44
+ end
45
+
46
+ def signal_done
47
+ synchronize { @resource.signal }
54
48
  end
55
49
 
56
- # Blocking. Starts a new thread with the given block when a pool's slot
57
- # become available.
58
- def defer(&block)
59
- index = wait_available_slot
60
- @threads[index].kill if @threads[index].respond_to?(:kill)
61
- @threads[index] = Thread.new(&block)
50
+ def thread_proc(&block)
51
+ Proc.new do
52
+ block.call
53
+ @running.reject! { |thread| thread == Thread.current }
54
+ process_queue
55
+ signal_done if done?
56
+ end
62
57
  end
63
58
  end
64
59
  end
data/lib/rmega/session.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'rmega/storage'
2
- require 'rmega/request_error'
2
+ require 'rmega/errors'
3
3
  require 'rmega/crypto/crypto'
4
4
  require 'rmega/utils'
5
5
 
@@ -11,11 +11,12 @@ module Rmega
11
11
  class Session
12
12
  include Loggable
13
13
 
14
- attr_reader :email, :request_id, :sid, :master_key
14
+ attr_reader :email, :request_id, :sid, :master_key, :shared_keys, :rsa_privk
15
15
 
16
16
  def initialize(email, password)
17
17
  @email = email
18
18
  @request_id = random_request_id
19
+ @shared_keys = {}
19
20
 
20
21
  login(password)
21
22
  end
@@ -25,6 +26,7 @@ module Rmega
25
26
  end
26
27
 
27
28
  delegate :api_url, :api_request_timeout, to: :options
29
+ delegate :max_retries, :retry_interval, to: :options
28
30
 
29
31
  def storage
30
32
  @storage ||= Storage.new(self)
@@ -39,7 +41,8 @@ module Rmega
39
41
  @master_key = Crypto.decrypt_key(encrypted_key, Utils.base64_to_a32(resp['k']))
40
42
 
41
43
  # Generates the session id
42
- @sid = Crypto.decrypt_sid(@master_key, resp['csid'], resp['privk'])
44
+ @rsa_privk = Crypto.decrypt_rsa_privk(@master_key, resp['privk'])
45
+ @sid = Crypto.decrypt_sid(@rsa_privk, resp['csid'])
43
46
  end
44
47
 
45
48
  def random_request_id
@@ -52,14 +55,28 @@ module Rmega
52
55
  end
53
56
  end
54
57
 
55
- def request(body)
58
+ def request(content, retries = max_retries)
56
59
  @request_id += 1
57
- logger.debug "POST #{request_url}\n#{body.inspect}"
58
- response = HTTPClient.new.post(request_url, [body].to_json, timeout: api_request_timeout)
59
- logger.debug "#{response.code}\n#{response.body}"
60
- resp = JSON.parse(response.body).first
61
- raise RequestError.new(resp) if RequestError.error_code?(resp)
62
- resp
60
+ logger.debug "POST #{request_url} #{content.inspect}"
61
+
62
+ response = HTTPClient.new.post(request_url, [content].to_json, timeout: api_request_timeout)
63
+ code, body = response.code.to_i, response.body
64
+
65
+ logger.debug("#{code} #{body}")
66
+
67
+ if code == 500 && body.to_s.empty?
68
+ raise Errors::ServerError.new("Server too busy", temporary: true)
69
+ else
70
+ json = JSON.parse(body).first
71
+ raise Errors::ServerError.new(json) if json.to_s =~ /\A\-\d+\z/
72
+ json
73
+ end
74
+ rescue SocketError, Errors::ServerError => error
75
+ raise(error) if retries < 0
76
+ raise(error) if error.respond_to?(:temporary?) && !error.temporary?
77
+ retries -= 1
78
+ sleep(retry_interval)
79
+ retry
63
80
  end
64
81
  end
65
82
  end
data/lib/rmega/storage.rb CHANGED
@@ -24,7 +24,20 @@ module Rmega
24
24
 
25
25
  def nodes
26
26
  results = session.request(a: 'f', c: 1)['f']
27
- results.map { |node_data| Nodes::Factory.build(session, node_data) }
27
+
28
+ results.map do |node_data|
29
+ node = Nodes::Factory.build(session, node_data)
30
+ node.process_shared_key if node.shared_root?
31
+ node
32
+ end
33
+ end
34
+
35
+ def folders
36
+ list = nodes
37
+ root_handle = list.find { |node| node.type == :root }.handle
38
+ list.select do |node|
39
+ node.shared_root? || (node.type == :folder && node.parent_handle == root_handle)
40
+ end
28
41
  end
29
42
 
30
43
  def trash
data/lib/rmega/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rmega
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6"
3
3
  end
@@ -12,7 +12,7 @@ describe 'Login' do
12
12
 
13
13
  context 'when email and password are invalid' do
14
14
  it 'raises an error' do
15
- expect { Rmega.login('a@apple.com', 'b') }.to raise_error(Rmega::RequestError)
15
+ expect { Rmega.login('a@apple.com', 'b') }.to raise_error(Rmega::Errors::ServerError)
16
16
  end
17
17
  end
18
18
  end
@@ -0,0 +1,2 @@
1
+ email: insert_email_here
2
+ password: insert_pass_here
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rmega
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniele Molteni
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-14 00:00:00.000000000 Z
11
+ date: 2014-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -102,6 +102,7 @@ extensions: []
102
102
  extra_rdoc_files: []
103
103
  files:
104
104
  - .gitignore
105
+ - CHANGELOG.md
105
106
  - Gemfile
106
107
  - LICENSE
107
108
  - README.md
@@ -113,9 +114,10 @@ files:
113
114
  - lib/rmega/crypto/crypto.rb
114
115
  - lib/rmega/crypto/rsa.rb
115
116
  - lib/rmega/crypto/rsa_mega.js
116
- - lib/rmega/downloader.rb
117
+ - lib/rmega/errors.rb
117
118
  - lib/rmega/loggable.rb
118
119
  - lib/rmega/nodes/deletable.rb
120
+ - lib/rmega/nodes/downloadable.rb
119
121
  - lib/rmega/nodes/expandable.rb
120
122
  - lib/rmega/nodes/factory.rb
121
123
  - lib/rmega/nodes/file.rb
@@ -125,13 +127,12 @@ files:
125
127
  - lib/rmega/nodes/root.rb
126
128
  - lib/rmega/nodes/trash.rb
127
129
  - lib/rmega/nodes/traversable.rb
130
+ - lib/rmega/nodes/uploadable.rb
128
131
  - lib/rmega/options.rb
129
132
  - lib/rmega/pool.rb
130
133
  - lib/rmega/progress.rb
131
- - lib/rmega/request_error.rb
132
134
  - lib/rmega/session.rb
133
135
  - lib/rmega/storage.rb
134
- - lib/rmega/uploader.rb
135
136
  - lib/rmega/utils.rb
136
137
  - lib/rmega/version.rb
137
138
  - rmega.gemspec
@@ -139,6 +140,7 @@ files:
139
140
  - spec/integration/file_upload_spec.rb
140
141
  - spec/integration/folder_operations_spec.rb
141
142
  - spec/integration/login_spec.rb
143
+ - spec/integration/rmega_account.yml.example
142
144
  - spec/integration_spec_helper.rb
143
145
  - spec/rmega/lib/crypto/aes_spec.rb
144
146
  - spec/rmega/lib/crypto/crypto_spec.rb
@@ -173,6 +175,7 @@ test_files:
173
175
  - spec/integration/file_upload_spec.rb
174
176
  - spec/integration/folder_operations_spec.rb
175
177
  - spec/integration/login_spec.rb
178
+ - spec/integration/rmega_account.yml.example
176
179
  - spec/integration_spec_helper.rb
177
180
  - spec/rmega/lib/crypto/aes_spec.rb
178
181
  - spec/rmega/lib/crypto/crypto_spec.rb
@@ -1,70 +0,0 @@
1
- require 'rmega/loggable'
2
- require 'rmega/utils'
3
- require 'rmega/pool'
4
- require 'rmega/progress'
5
-
6
- module Rmega
7
- class Downloader
8
- include Loggable
9
-
10
- attr_reader :pool, :base_url, :filesize, :local_path
11
-
12
- def initialize(params)
13
- @pool = Pool.new(params[:threads])
14
- @filesize = params[:filesize]
15
- @base_url = params[:base_url]
16
- @local_path = params[:local_path]
17
- end
18
-
19
- # Creates the local file allocating filesize-n bytes (of /dev/zero) for it.
20
- # Opens the local file to start writing from the beginning of it.
21
- def allocate
22
- `dd if=/dev/zero of="#{local_path}" bs=1 count=0 seek=#{filesize} > /dev/null 2>&1`
23
- raise "Unable to create file #{local_path}" if File.size(local_path) != filesize
24
-
25
- ::File.open(local_path, 'r+b').tap { |f| f.rewind }
26
- end
27
-
28
- # Downloads a part of the remote file, starting from the start-n byte
29
- # and ending after size-n bytes.
30
- def download_chunk(start, size)
31
- stop = start + size - 1
32
- url = "#{base_url}/#{start}-#{stop}"
33
- HTTPClient.new.get_content(url)
34
- end
35
-
36
- # Writes a buffer in the local file, starting from the start-n byte.
37
- def write_chunk(start, buffer)
38
- @local_file.seek(start)
39
- @local_file.write(buffer)
40
- end
41
-
42
- def chunks
43
- Utils.chunks(filesize)
44
- end
45
-
46
- def download(&block)
47
- @local_file = allocate
48
-
49
- progress = Progress.new(total: filesize, caption: 'Download')
50
-
51
- chunks.each do |start, size|
52
- pool.defer do
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) }
57
- end
58
- end
59
-
60
- # waits for the last running threads to finish
61
- pool.wait_done
62
-
63
- @local_file.flush
64
-
65
- pool.shutdown
66
- ensure
67
- @local_file.close rescue nil
68
- end
69
- end
70
- end
@@ -1,35 +0,0 @@
1
- module Rmega
2
- class RequestError < StandardError
3
- def initialize(error_code)
4
- message = self.class.errors[error_code]
5
- super("Error #{error_code}: #{message}")
6
- end
7
-
8
- def self.error_code?(number)
9
- number.respond_to?(:to_i) and number.to_i < 0
10
- end
11
-
12
- def self.errors
13
- {
14
- -1 => 'An internal error has occurred. Please submit a bug report, detailing the exact circumstances in which this error occurred.',
15
- -2 => 'You have passed invalid arguments to this command.',
16
- -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.',
17
- -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).',
18
- -5 => 'The upload failed. Please restart it from scratch.',
19
- -6 => 'Too many concurrent IP addresses are accessing this upload target URL.',
20
- -7 => 'The upload file packet is out of range or not starting and ending on a chunk boundary.',
21
- -8 => 'The upload target URL you are trying to access has expired. Please request a fresh one.',
22
- -9 => 'Object (typically, node or user) not found',
23
- -10 => 'Circular linkage attempted',
24
- -11 => 'Access violation (e.g., trying to write to a read-only share)',
25
- -12 => 'Trying to create an object that already exists',
26
- -13 => 'Trying to access an incomplete resource',
27
- -14 => 'A decryption operation failed (never returned by the API)',
28
- -15 => 'Invalid or expired user session, please relogin',
29
- -16 => 'User blocked',
30
- -17 => 'Request over quota',
31
- -18 => 'Resource temporarily not available, please try again later'
32
- }
33
- end
34
- end
35
- end
@@ -1,58 +0,0 @@
1
- require 'rmega/loggable'
2
- require 'rmega/utils'
3
- require 'rmega/pool'
4
- require 'rmega/progress'
5
-
6
- module Rmega
7
- class Uploader
8
- include Loggable
9
-
10
- attr_reader :pool, :base_url, :filesize, :local_path, :last_result
11
-
12
- def initialize(params)
13
- @pool = Pool.new(params[:threads])
14
- @filesize = params[:filesize]
15
- @base_url = params[:base_url]
16
- @local_path = params[:local_path]
17
- @last_result = nil
18
- end
19
-
20
- def upload_chunk(start, buffer)
21
- size = buffer.length
22
- stop = start + size - 1
23
- url = "#{base_url}/#{start}-#{stop}"
24
-
25
- HTTPClient.new.post(url, buffer).body
26
- end
27
-
28
- def read_chunk(start, size)
29
- @local_file.seek(start)
30
- @local_file.read(size)
31
- end
32
-
33
- def chunks
34
- Utils.chunks(filesize)
35
- end
36
-
37
- def upload(&block)
38
- @local_file = ::File.open(local_path, 'rb')
39
-
40
- progress = Progress.new(total: filesize, caption: 'Upload')
41
-
42
- chunks.each do |start, size|
43
-
44
- pool.defer do
45
- clean_buffer = pool.synchronize { read_chunk(start, size) }
46
- encrypted_buffer = yield(start, clean_buffer)
47
- @last_result = upload_chunk(start, encrypted_buffer)
48
- progress.increment(clean_buffer.size)
49
- end
50
- end
51
-
52
- pool.wait_done
53
- pool.shutdown
54
- ensure
55
- @local_file.close
56
- end
57
- end
58
- end