rmega 0.1.5 → 0.1.6

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