rmega 0.0.6 → 0.1.0

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