rmega 0.0.6 → 0.1.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.
@@ -1,122 +1,80 @@
1
+ require 'rmega/loggable'
2
+ require 'rmega/utils'
3
+ require 'rmega/crypto/crypto'
4
+ require 'rmega/nodes/traversable'
5
+
1
6
  module Rmega
2
- class Node
3
- attr_reader :data, :session
7
+ module Nodes
8
+ class Node
9
+ include Loggable
10
+ include Traversable
11
+
12
+ attr_reader :data, :session
4
13
 
5
- def initialize session, data
6
- @session = session
14
+ delegate :storage, :request, :to => :session
7
15
 
8
- if self.class.mega_url?(data)
9
- @data = self.class.public_data(session, data)
10
- @public_url = data
11
- else
16
+ def initialize(session, data)
17
+ @session = session
12
18
  @data = data
13
19
  end
14
- end
15
-
16
- def self.fabricate session, data
17
- type_name = mega_url?(data) ? :file : type_by_number(data['t'])
18
- node_class = Rmega.const_get("#{type_name}_node".camelize) rescue nil
19
- node_class ||= Rmega::Node
20
- node_class.new session, data
21
- end
22
-
23
- def self.types
24
- {file: 0, folder: 1, root: 2, inbox: 3, trash: 4}
25
- end
26
-
27
- def self.type_by_number number
28
- founded_type = types.find { |k, v| number == v }
29
- founded_type.first if founded_type
30
- end
31
-
32
- def self.mega_url? url
33
- !!(url.to_s =~ /^https:\/\/mega\.co\.nz\/#!.*$/i)
34
- end
35
-
36
- def logger
37
- Rmega.logger
38
- end
39
-
40
- # Member actions
41
-
42
- def public_url
43
- return @public_url if @public_url
44
- return nil if type != :file
45
- b64_dec_key = Utils.a32_to_base64 decrypted_file_key[0..7]
46
- "https://mega.co.nz/#!#{public_handle}!#{b64_dec_key}"
47
- end
48
-
49
- def trash
50
- trash_node_public_handle = storage.trash_node.public_handle
51
- request a: 'm', n: handle, t: trash_node_public_handle
52
- end
53
-
54
-
55
- # Delegate to session
56
-
57
- delegate :storage, :request, :to => :session
58
-
59
-
60
- # Other methods
61
20
 
62
- def self.public_data session, public_url
63
- public_handle, key = public_url.strip.split('!')[1, 2]
64
- session.request a: 'g', g: 1, p: public_handle
65
- end
66
-
67
- def public_handle
68
- @public_handle ||= request(a: 'l', n: handle)
69
- end
21
+ def public_url
22
+ @public_url ||= begin
23
+ b64_dec_key = Utils.a32_to_base64 decrypted_file_key[0..7]
24
+ "https://mega.co.nz/#!#{public_handle}!#{b64_dec_key}"
25
+ end
26
+ end
70
27
 
71
- def handle
72
- data['h']
73
- end
28
+ def public_url=(url)
29
+ @public_url = url
30
+ end
74
31
 
75
- def parent_handle
76
- data['p']
77
- end
32
+ def public_handle
33
+ @public_handle ||= request(a: 'l', n: handle)
34
+ end
78
35
 
79
- def filesize
80
- data['s']
81
- end
36
+ def handle
37
+ data['h']
38
+ end
82
39
 
83
- def owner_key
84
- data['k'].split(':').first
85
- end
40
+ def parent_handle
41
+ data['p']
42
+ end
86
43
 
87
- def name
88
- return attributes['n'] if attributes
89
- end
44
+ def owner_key
45
+ data['k'].split(':').first
46
+ end
90
47
 
91
- def file_key
92
- data['k'].split(':').last
93
- end
48
+ def name
49
+ return attributes['n'] if attributes
50
+ end
94
51
 
95
- def decrypted_file_key
96
- if data['k']
97
- Crypto.decrypt_key session.master_key, Utils.base64_to_a32(file_key)
98
- else
99
- Utils.base64_to_a32 public_url.split('!').last
52
+ def file_key
53
+ data['k'].split(':').last
100
54
  end
101
- end
102
55
 
103
- def can_decrypt_attributes?
104
- !data['u'] or data['u'] == owner_key
105
- end
56
+ def decrypted_file_key
57
+ if data['k']
58
+ Crypto.decrypt_key session.master_key, Utils.base64_to_a32(file_key)
59
+ else
60
+ Utils.base64_to_a32 public_url.split('!').last
61
+ end
62
+ end
106
63
 
107
- def attributes
108
- @attributes ||= begin
109
- return nil unless can_decrypt_attributes?
110
- Crypto.decrypt_attributes decrypted_file_key, (data['a'] || data['at'])
64
+ def can_decrypt_attributes?
65
+ !data['u'] or data['u'] == owner_key
111
66
  end
112
- end
113
67
 
114
- def type
115
- self.class.type_by_number data['t']
116
- end
68
+ 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
73
+ end
117
74
 
118
- def delete
119
- request a: 'd', n: handle
75
+ def type
76
+ Factory.type(data['t'])
77
+ end
120
78
  end
121
79
  end
122
80
  end
@@ -0,0 +1,12 @@
1
+ require 'rmega/nodes/node'
2
+ require 'rmega/nodes/expandable'
3
+ require 'rmega/nodes/traversable'
4
+
5
+ module Rmega
6
+ module Nodes
7
+ class Root < Node
8
+ include Expandable
9
+ include Traversable
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ require 'rmega/nodes/node'
2
+ require 'rmega/nodes/traversable'
3
+
4
+ module Rmega
5
+ module Nodes
6
+ class Trash < Node
7
+ include Traversable
8
+
9
+ def empty!
10
+ children.each do |node|
11
+ node.delete if node.respond_to?(:delete)
12
+ end
13
+
14
+ empty?
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ module Rmega
2
+ module Nodes
3
+ module Traversable
4
+ def children
5
+ storage.nodes.select { |node| node.parent_handle == handle }
6
+ end
7
+
8
+ def folders
9
+ children.select { |node| node.type == :folder }
10
+ end
11
+
12
+ def files
13
+ children.select { |node| node.type == :file }
14
+ end
15
+
16
+ def parent
17
+ return unless parent_handle
18
+ storage.nodes.find { |node| node.handle == parent_handle }
19
+ end
20
+
21
+ def empty?
22
+ children.size == 0
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ require 'ostruct'
2
+
3
+ module Rmega
4
+ def self.default_options
5
+ {
6
+ show_progress: true,
7
+ upload_timeout: 120,
8
+ api_request_timeout: 20,
9
+ api_url: 'https://eu.api.mega.co.nz/cs'
10
+ }
11
+ end
12
+
13
+ def self.options
14
+ @options ||= OpenStruct.new(default_options)
15
+ end
16
+ end
data/lib/rmega/pool.rb CHANGED
@@ -1,13 +1,11 @@
1
1
  require 'thread'
2
2
 
3
- class Thread
4
- # Helper to create a Pool instance.
5
- def self.pool(max)
6
- Pool.new(max)
7
- end
8
-
3
+ module Rmega
9
4
  class Pool
5
+ MAX = 5
6
+
10
7
  def initialize(max)
8
+ max ||= MAX
11
9
  Thread.abort_on_exception = true
12
10
  @mutex = Mutex.new
13
11
  @threads = Array.new(max)
@@ -0,0 +1,34 @@
1
+ module Rmega
2
+ class Progress
3
+
4
+ def initialize(params)
5
+ @filesize = params[:filesize]
6
+ @verb = params[:verb].capitalize
7
+ @progress = 0
8
+
9
+ render
10
+ end
11
+
12
+ def render
13
+ percentage = (100.0 * @progress / @filesize).round(2)
14
+ message = "#{@verb} in progress #{format_bytes(@progress)} of #{format_bytes(@filesize)} (#{percentage}%)"
15
+ rtrn = "\n" if @filesize == @progress
16
+
17
+ print "\r#{' '*(message.size + 15)}\r#{message}#{rtrn}"
18
+ end
19
+
20
+ def increment(bytes)
21
+ @progress += bytes
22
+
23
+ render
24
+ end
25
+
26
+ def format_bytes(bytes, round = 2)
27
+ units = ['bytes', 'kb', 'MB', 'GB', 'TB', 'PB']
28
+ e = (bytes == 0 ? 0 : Math.log(bytes)) / Math.log(1024)
29
+ value = bytes.to_f / (1024 ** e.floor)
30
+
31
+ "#{value.round(round)}#{units[e]}"
32
+ end
33
+ end
34
+ end
@@ -1,18 +1,15 @@
1
1
  module Rmega
2
- class ApiRequestError < StandardError
3
- attr_reader :code
4
-
5
- def initialize error_code
6
- @code = error_code
7
- error_description = self.class.all_messages[@code]
8
- super "Received error code #{@code}. #{error_description}".strip
2
+ class RequestError < StandardError
3
+ def initialize(error_code)
4
+ message = self.class.errors[error_code]
5
+ super("Error #{error_code}: #{message}")
9
6
  end
10
7
 
11
- def self.is_error_code? number
12
- (Integer(number) && number.to_i < 0) rescue false
8
+ def self.error_code?(number)
9
+ number.respond_to?(:to_i) and number.to_i < 0
13
10
  end
14
11
 
15
- def self.all_messages
12
+ def self.errors
16
13
  {
17
14
  -1 => 'An internal error has occurred. Please submit a bug report, detailing the exact circumstances in which this error occurred.',
18
15
  -2 => 'You have passed invalid arguments to this command.',
data/lib/rmega/session.rb CHANGED
@@ -1,63 +1,52 @@
1
+ require 'rmega/storage'
2
+ require 'rmega/request_error'
3
+ require 'rmega/crypto/crypto'
4
+ require 'rmega/utils'
5
+
1
6
  module Rmega
7
+ def self.login(email, password)
8
+ Session.new(email, password).storage
9
+ end
10
+
2
11
  class Session
12
+ include Loggable
13
+
3
14
  attr_accessor :email, :request_id, :sid, :master_key
4
15
 
5
- def initialize email, password_str
16
+ def initialize(email, password)
6
17
  self.email = email
7
18
  self.request_id = random_request_id
8
19
 
9
- login password_str
20
+ login(password)
10
21
  end
11
22
 
12
- def logger
13
- Rmega.logger
14
- end
15
-
16
-
17
- # Delegate to Rmega.options
18
-
19
23
  def options
20
24
  Rmega.options
21
25
  end
22
26
 
23
- def api_request_timeout
24
- options.api_request_timeout
25
- end
26
-
27
- def api_url
28
- options.api_url
29
- end
30
-
31
-
32
- # Cache the Storage class
27
+ delegate :api_url, :api_request_timeout, to: :options
33
28
 
34
29
  def storage
35
- @storage ||= Storage.new self
30
+ @storage ||= Storage.new(self)
36
31
  end
37
32
 
38
-
39
- # Login-related methods
40
-
41
- def login password_str
42
- uh = Crypto.stringhash Crypto.prepare_key_pw(password_str), email
33
+ def login(password)
34
+ uh = Crypto.stringhash Crypto.prepare_key_pw(password), email
43
35
  resp = request a: 'us', user: email, uh: uh
44
36
 
45
37
  # Decrypt the master key
46
- encrypted_key = Crypto.prepare_key_pw password_str
38
+ encrypted_key = Crypto.prepare_key_pw password
47
39
  self.master_key = Crypto.decrypt_key encrypted_key, Utils.base64_to_a32(resp['k'])
48
40
 
49
41
  # Generate the session id
50
42
  self.sid = Crypto.decrypt_sid master_key, resp['csid'], resp['privk']
51
43
  end
52
44
 
53
-
54
- # Api requests methods
55
-
56
45
  def random_request_id
57
46
  rand(1E7..1E9).to_i
58
47
  end
59
48
 
60
- def request body
49
+ def request(body)
61
50
  self.request_id += 1
62
51
  url = "#{api_url}?id=#{request_id}"
63
52
  url << "&sid=#{sid}" if sid
@@ -66,7 +55,7 @@ module Rmega
66
55
  response = HTTPClient.new.post url, [body].to_json, timeout: api_request_timeout
67
56
  logger.debug "#{response.code}\n#{response.body}"
68
57
  resp = JSON.parse(response.body).first
69
- raise ApiRequestError.new(resp) if ApiRequestError.is_error_code?(resp)
58
+ raise RequestError.new(resp) if RequestError.error_code?(resp)
70
59
  resp
71
60
  end
72
61
  end
data/lib/rmega/storage.rb CHANGED
@@ -1,19 +1,17 @@
1
+ require 'rmega/utils'
2
+ require 'rmega/crypto/crypto'
3
+ require 'rmega/nodes/factory'
4
+
1
5
  module Rmega
2
6
  class Storage
7
+ include Loggable
3
8
 
4
9
  attr_reader :session
5
10
 
6
- def initialize session
11
+ def initialize(session)
7
12
  @session = session
8
13
  end
9
14
 
10
- def logger
11
- Rmega.logger
12
- end
13
-
14
-
15
- # Quota-related methods
16
-
17
15
  def used_space
18
16
  quota['cstrg']
19
17
  end
@@ -26,81 +24,34 @@ module Rmega
26
24
  session.request a: 'uq', strg: 1
27
25
  end
28
26
 
29
-
30
- # Nodes management
31
-
32
27
  def nodes
33
- nodes = session.request a: 'f', c: 1
34
- nodes['f'].map { |node_data| Node.fabricate(session, node_data) }
35
- end
36
-
37
- def nodes_by_type type
38
- nodes.select { |n| n.type == type }
39
- end
40
-
41
- def nodes_by_name name_regexp
42
- nodes.select do |node|
43
- node.name and node.name =~ name_regexp
44
- end
45
- end
46
-
47
- def trash_node
48
- @trash ||= nodes_by_type(:trash).first
28
+ result = session.request(a: 'f', c: 1)
29
+ result['f'].map { |node_data| Nodes::Factory.build(session, node_data) }
49
30
  end
50
31
 
51
- def root_node
52
- @root_node ||= nodes_by_type(:root).first
32
+ def trash
33
+ @trash ||= nodes.find { |n| n.type == :trash }
53
34
  end
54
35
 
55
- def create_folder parent_node, folder_name
56
- FolderNode.create session, parent_node, folder_name
36
+ def root
37
+ @root_node ||= nodes.find { |n| n.type == :root }
57
38
  end
58
39
 
59
-
60
- # Handle node download
61
-
62
- def self.chunks size
63
- list = {}
64
- p = 0
65
- pp = 0
66
- i = 1
67
-
68
- while i <= 8 and p < size - (i * 0x20000)
69
- list[p] = i * 0x20000
70
- pp = p
71
- p += list[p]
72
- i += 1
73
- end
74
-
75
- while p < size
76
- list[p] = 0x100000
77
- pp = p
78
- p += list[p]
79
- end
80
-
81
- if size - pp > 0
82
- list[pp] = size - pp
83
- end
84
- list
85
- end
86
-
87
- def download public_url, path
88
- Node.fabricate(session, public_url).download(path)
40
+ def download(public_url, path)
41
+ Nodes::Factory.build_from_url(session, public_url).download(path)
89
42
  end
90
43
 
91
-
92
- # Handle file upload
93
-
94
- def upload_url filesize
44
+ # TODO: refactor upload part
45
+ def upload_url(filesize)
95
46
  session.request(a: 'u', s: filesize)['p']
96
47
  end
97
48
 
98
- def upload_chunk url, start, chunk
49
+ def upload_chunk(url, start, chunk)
99
50
  response = HTTPClient.new.post "#{url}/#{start}", chunk, timeout: Rmega.options.upload_timeout
100
51
  response.body
101
52
  end
102
53
 
103
- def upload local_path, parent_node = root_node
54
+ def upload(local_path, parent_node = root)
104
55
  local_path = File.expand_path local_path
105
56
  filesize = File.size local_path
106
57
  upld_url = upload_url filesize
@@ -114,7 +65,7 @@ module Rmega
114
65
 
115
66
  Utils.show_progress :upload, filesize
116
67
 
117
- self.class.chunks(filesize).each do |chunk_start, chunk_size|
68
+ Utils.chunks(filesize).each do |chunk_start, chunk_size|
118
69
  buffer = local_file.read chunk_size
119
70
 
120
71
  # TODO: should be (chunk_start/0x1000000000) >>> 0, (chunk_start/0x10) >>> 0
@@ -0,0 +1,58 @@
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(filesize: filesize, verb: 'upload')
41
+
42
+ chunks.each do |start, size|
43
+
44
+ pool.defer do
45
+ clean_buffer = pool.syncronize { read_chunk(start, size) }
46
+ encrypted_buffer = yield(start, clean_buffer)
47
+ @last_result = upload_chunk(start, encrypted_buffer)
48
+ progress.increment(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