rapidshare-ext 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.
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env rake
2
- require "bundler/gem_tasks"
2
+ require 'bundler/gem_tasks'
3
3
  require 'rake/testtask'
4
4
  include Rake::DSL
5
5
 
6
- desc "Run tests"
7
- task :test => ['test:unit', 'test:integration']
6
+ desc 'Run tests'
7
+ task :test => %w{test:unit test:integration}
8
8
 
9
9
  task :default => :test
10
10
 
@@ -1,5 +1,51 @@
1
1
  module Rapidshare
2
2
  class API
3
+ include Rapidshare::Utils
4
+ extend Rapidshare::Utils
5
+
6
+ attr_reader :cookie
7
+
8
+ ERROR_PREFIX = 'ERROR: ' unless defined?(ERROR_PREFIX)
9
+
10
+ # custom errors for Rapidshare::API class
11
+ class Error < StandardError; end
12
+ class Error::LoginFailed < StandardError; end
13
+ class Error::InvalidRoutineCalled < StandardError; end
14
+
15
+ # Request method uses this string to construct GET requests
16
+ #
17
+ URL = 'https://api.rapidshare.com/cgi-bin/rsapi.cgi?sub=%s&%s'
18
+
19
+ # Connects to Rapidshare API (which basically means: uses login and password
20
+ # to retrieve cookie for future service calls)
21
+ #
22
+ # Params:
23
+ # * *login* - premium account login
24
+ # * *password* - premium account password
25
+ # * *cookie* - cookie can be provided instead of login and password
26
+ # * *free_user* (boolean) - if user identifies himself as free user by setting this to *true*, skip login
27
+ #
28
+ # Instead of params hash, you can pass only cookie as a string
29
+ #
30
+ # PS: *free_user* option is a beta feature, to be properly implemented
31
+ #
32
+ def initialize(params)
33
+ # if there's "just one param", it's a cookie
34
+ params = { :cookie => params } if params.is_a? String
35
+
36
+ # skip login for free users
37
+ return nil if params[:free_user]
38
+
39
+ if params[:cookie]
40
+ @cookie = params[:cookie]
41
+ # throws LoginFailed exception if cookie is invalid
42
+ get_account_details()
43
+ else
44
+ response = get_account_details(params.merge(:withcookie => 1))
45
+ @cookie = response[:cookie]
46
+ end
47
+ end
48
+
3
49
  # TODO this class is getting long. keep general request-related and helper
4
50
  # method here and move specific method calls like :getaccountdetails to other
5
51
  # class (service)?
@@ -18,7 +64,7 @@ module Rapidshare
18
64
  # * *none* - default value, returns response body as it is
19
65
  # * *csv* - comma-separated values, for example: _getrapidtranslogs_.
20
66
  # Returns array or arrays, one array per each line.
21
- # * *hash* - lines with key and value separated by "=", for example:
67
+ # * *hash* - lines with key and value separated by '=', for example:
22
68
  # _getaccountdetails_. Returns hash.
23
69
  # * *server* - option, determines which server will be used to send request
24
70
  #
@@ -45,10 +91,10 @@ module Rapidshare
45
91
  end
46
92
 
47
93
  if response.start_with?(ERROR_PREFIX)
48
- case error = response.sub(ERROR_PREFIX, "").split('.').first
49
- when "Login failed"
94
+ case error = response.sub(ERROR_PREFIX, '').split('.').first
95
+ when 'Login failed'
50
96
  raise Rapidshare::API::Error::LoginFailed
51
- when "Invalid routine called"
97
+ when 'Invalid routine called'
52
98
  raise Rapidshare::API::Error::InvalidRoutineCalled.new(service_name)
53
99
  else
54
100
  raise Rapidshare::API::Error.new(error)
@@ -58,14 +104,98 @@ module Rapidshare
58
104
  self.parse_response(parser, response)
59
105
  end
60
106
 
61
- def download(file, params= {})
107
+ # Provides instance interface to class method +request+.
108
+ #
109
+ def request(service_name, params = {})
110
+ self.class.request(service_name, params.merge(:cookie => @cookie))
111
+ end
112
+
113
+ # Parses response from +request+ method (parser options are listed there)
114
+ #
115
+ def self.parse_response(parser, response)
116
+ case parser.to_sym
117
+ when :none
118
+ response
119
+ when :csv
120
+ # PS: we could use gem for csv parsing, but that's an overkill in this
121
+ # case, IMHO
122
+ response.to_s.strip.split(/\s*\n\s*/).map { |line| line.split(',') }
123
+ when :hash
124
+ text_to_hash(response)
125
+ end
126
+ end
127
+
128
+ # Attempts to do Rapidshare service call. If it doesn't recognize the method
129
+ # name, this gem assumes that user wants to make a Rapidshare service call.
130
+ #
131
+ # This method also handles aliases for service calls:
132
+ # get_account_details -> getaccountdetails
133
+ #
134
+ def method_missing(service_name, params = {})
135
+ # remove user-friendly underscores from service names
136
+ service_name = service_name.to_s.gsub('_', '')
137
+
138
+ if respond_to?(service_name)
139
+ send(service_name, params)
140
+ else
141
+ request(service_name, params)
142
+ end
143
+ end
144
+
145
+ # Returns account details in hash.
146
+ #
147
+ def getaccountdetails(params = {})
148
+ request :getaccountdetails, params.merge( :parser => :hash)
149
+ end
150
+
151
+ # Retrieves information about RapidShare files.
152
+ #
153
+ # *Input:* array of files
154
+ #
155
+ # Examples: +checkfiles(file1)+, +checkfiles(file1,file2)+ or +checkfiles([file1,file2])+
156
+ #
157
+ # *Output:* array of hashes, which contain information about files
158
+ # * *:file_id* (string) - part of url
159
+ #
160
+ # Example: https://rapidshare.com/files/829628035/HornyRhinos.jpg -> +829628035+
161
+ # * *:file_name* (string) - part of url
162
+ #
163
+ # Example: https://rapidshare.com/files/829628035/HornyRhinos.jpg -> +HornyRhinos.jpg+
164
+ # * *:file_size* (integer) - in bytes. returns 0 if files does not exists
165
+ # * *:file_status* - decoded file status: +:ok+ or +:error+
166
+ # * *:short_host* - used to construct download url
167
+ # * *:server_id* - used to construct download url
168
+ # * *:md5*
169
+ #
170
+ def checkfiles(*urls)
171
+ raise Rapidshare::API::Error if urls.empty?
172
+
173
+ files, filenames = urls.flatten.map { |url| fileid_and_filename(url) }.transpose
174
+
175
+ response = request(:checkfiles, :files => files.join(','), :filenames => filenames.join(','))
176
+
177
+ response.strip.split(/\s*\n\s*/).map do |r|
178
+ data = r.split ','
179
+ {
180
+ :file_id => data[0],
181
+ :file_name => data[1],
182
+ :file_size => data[2],
183
+ :server_id => data[3],
184
+ :file_status => decode_file_status(data[4].to_i),
185
+ :short_host => data[5],
186
+ :md5 => data[6]
187
+ }
188
+ end
189
+ end
190
+
191
+ def download(file, params= {}, &block)
62
192
  if file.match /\Ahttps?:\/\//
63
193
  url = file
64
194
  else
65
195
  url = file_info(file)[:url]
66
196
  end
67
197
 
68
- Rapidshare::Ext::Download.new(url, self, params).perform
198
+ Rapidshare::Ext::Download.new(url, self, params).perform &block
69
199
  end
70
200
  end
71
201
  end
@@ -1,5 +1,11 @@
1
1
  module Rapidshare
2
+
3
+ # Contains utility methods which can be called both as class and instance methods
4
+ #
2
5
  module Utils
6
+
7
+ # From historical purposes GET and POST methods a distinguished.
8
+ # They will be merged in a future release
3
9
  def post(url, params)
4
10
  params[:filecontent] = File.new(params[:filecontent])
5
11
  ::RestClient.post(url, params)
@@ -8,5 +14,38 @@ module Rapidshare
8
14
  def get(url)
9
15
  ::RestClient.get(url)
10
16
  end
17
+
18
+ # Convert file status code (returned by checkfiles method) to +:ok+ or +:error+ symbol.
19
+ #
20
+ def decode_file_status(status_code)
21
+ # TODO in checkfiles, return both file_status as is and decoded file status
22
+ # or just boolean value if file is OK and can be downloaded
23
+
24
+ case status_code
25
+ when 0 then :error # File not found
26
+ when 1 then :ok # File OK
27
+ when 3 then :error # Server down
28
+ when 4 then :error # File marked as illegal
29
+ else :error # unknown status, this shouldn't happen
30
+ end
31
+ end
32
+
33
+ # Extracts file id and file name from Rapidshare url. Returns both in array.
34
+ #
35
+ # Example:
36
+ # https://rapidshare.com/files/829628035/HornyRhinos.jpg -> [ '829628035', 'HornyRhinos.jpg' ]
37
+ #
38
+ def fileid_and_filename(url)
39
+ url.split('/').slice(-2,2) || ['', '']
40
+ end
41
+
42
+ # Converts rapidshare response (which is just a text in specific format) to hash.
43
+ #
44
+ # Example:
45
+ # "key1=value1\nkey1=value2" -> { :key1 => 'value1', :key2 => 'value2' }
46
+ #
47
+ def text_to_hash(response)
48
+ Hash[ response.strip.split(/\s*\n\s*/).map { |param| param.split('=') } ].symbolize_keys
49
+ end
11
50
  end
12
51
  end
@@ -1,11 +1,17 @@
1
- require 'rest-client'
2
- require 'rapidshare'
3
- require 'rapidshare-base/utils'
4
- require 'rapidshare-base/api'
5
- require 'rapidshare-ext/api'
6
- require 'rapidshare-ext/download'
7
- require 'rapidshare-ext/version'
8
-
9
- class Rapidshare::API
10
- include Rapidshare::Ext::API
11
- end
1
+ require 'rest-client'
2
+ #require 'progressbar'
3
+
4
+ # active_support helpers
5
+ require 'active_support/core_ext/object/to_query'
6
+ require 'active_support/core_ext/hash/keys'
7
+
8
+ require 'rapidshare-base/utils'
9
+ require 'rapidshare-base/api'
10
+
11
+ require 'rapidshare-ext/api'
12
+ require 'rapidshare-ext/download'
13
+ require 'rapidshare-ext/version'
14
+
15
+ class Rapidshare::API
16
+ include Rapidshare::Ext::API
17
+ end
@@ -1,532 +1,532 @@
1
- module Rapidshare
2
- module Ext
3
- module API
4
-
5
- FILE_COLUMNS = "downloads,lastdownload,filename,size,serverid,type,x,y,realfolder,killdeadline,uploadtime,comment,md5hex,licids,sentby"
6
-
7
- # @param [String] path Folder name with absolute path to be created
8
- # @param [Hash] params
9
- # @return [Integer]
10
- #
11
- # Creates a folder in a Rapidshare virtual filesystem
12
- #
13
- # api.add_folder("/a/b/c") #=> <Random folder ID from Rapidshare>, 1234 for example
14
- def add_folder(path, params = {})
15
- path = path_trim path
16
-
17
- @tree = folders_hierarchy
18
- i = 1
19
- parent = 0
20
- folder_id = nil
21
- while i <= path.split('/').count do
22
- base_path = path.split('/')[0,i].join('/')
23
- folder_id = self.folder_id base_path
24
- if folder_id
25
- parent = folder_id
26
- i += 1
27
- else
28
- # Create folder
29
- folder_name = path.split('/')[i-1]
30
- add_folder_params = {
31
- :name => folder_name,
32
- :parent => parent
33
- }.merge params
34
-
35
- # The following code deals with #{} because of rest client #to_i returns HTTP code
36
- folder_id = "#{addrealfolder(add_folder_params)}".to_i
37
- raise "error while creating folder" if parent < 0
38
- @tree[folder_id] = {
39
- :parent => parent,
40
- :name => folder_name,
41
- :path => path_canonize((@tree[parent] || {})[:path].to_s + ('/' if @tree[parent]).to_s + folder_name)
42
- }
43
- parent = folder_id
44
- path == base_path + '/' + folder_name
45
- i += 1
46
- next
47
- end
48
- end
49
- folder_id
50
- end
51
-
52
- # Removes a specified folder
53
- #
54
- # @param [String] path
55
- # @param [Hash] params
56
- # @return [Array]
57
- #
58
- # api.remove_folder("/a/b/c")
59
- def remove_folder(path, params = {})
60
- folder_id = self.folder_id path_trim(path)
61
- raise Exception, "Folder #{path} could not be found" if folder_id.nil?
62
-
63
- # TODO
64
- tree = folders_hierarchy :from => path
65
- tree.each_pair do |child_folder_id, data|
66
- delrealfolder_params = {
67
- :realfolder => child_folder_id
68
- }.merge params
69
-
70
- delrealfolder delrealfolder_params
71
- @tree.delete folder_id
72
- end
73
-
74
- params = {
75
- :realfolder => folder_id
76
- }.merge params
77
-
78
- delrealfolder params
79
-
80
- @tree.delete folder_id
81
- end
82
-
83
- # Moves folder into a specified one
84
- #
85
- # @param [String] source_path
86
- # @param [Hash] params
87
- # :to => <destination folder path>, default: "/"
88
- #
89
- # api.move_folder("/a/b/c", :to => "/a")
90
- def move_folder(source_path, params = {})
91
- dest_path = (params.delete(:to) || '/')
92
- source_folder_id = folder_id(source_path)
93
- dest_folder_id = folder_id(dest_path)
94
-
95
- params = {
96
- :realfolder => source_folder_id,
97
- :newparent => dest_folder_id
98
- }.merge params
99
-
100
- moverealfolder params
101
-
102
- @tree = folders_hierarchy
103
- @tree[source_folder_id][:parent] = dest_folder_id
104
- @tree[source_folder_id][:path] = path_canonize "#{folder_path(dest_folder_id)}/#{@tree[source_folder_id][:name]}"
105
- true
106
- end
107
-
108
- # Upload file to a specified folder
109
- #
110
- # @param [String] file_path
111
- # @param [Hash] params
112
- # <tt>:to</tt>::
113
- # Folder to place uploaded file to, default: "/"
114
- # <tt>:as</tt>::
115
- # The name file will have in storage after it has been uploaded
116
- # <tt>:overwrite</tt>::
117
- # Overwrite file if it already exists in the given folder
118
- #
119
- # api.upload("/home/odiszapc/my_damn_cat.mov", :to => "/gallery/video", :as => "cat1.mov")
120
- def upload(file_path, params = {})
121
- raise Exception unless File.exist? file_path
122
- dest_path = path_trim(params.delete(:to) || '/')
123
- folder_id = self.add_folder dest_path
124
- file_name = params.delete(:as) || File.basename(file_path)
125
- overwrite = params.delete :overwrite
126
-
127
- # Check file already exists within a folder
128
- listfiles_params = {
129
- :realfolder => folder_id,
130
- :filename => "#{file_name}",
131
- :fields => "md5hex,size",
132
- :parser => :csv
133
- }
134
- listfiles_response = self.listfiles listfiles_params
135
-
136
- file_already_exists = ("NONE" != listfiles_response[0][0])
137
- remove_file "#{dest_path}/#{file_name}" if file_already_exists && overwrite
138
-
139
- # In case of file is not existing then upload it
140
- if !file_already_exists || overwrite
141
- upload_server = "rs#{self.nextuploadserver}.rapidshare.com"
142
-
143
- upload_params = {
144
- :server => upload_server,
145
- :folder => folder_id,
146
- :filename => file_name,
147
- :filecontent => file_path,
148
- :method => :post,
149
- :parser => :csv
150
- }.merge params
151
-
152
- resp = request(:upload, upload_params)
153
- raise Exception, "File uploading failed: #{resp.inspect}" unless "COMPLETE" == resp[0][0]
154
-
155
- id = resp[1][0].to_i
156
- md5_hash = resp[1][3]
157
- size = resp[1][2].to_i
158
- already_exists = false
159
- else
160
- id = listfiles_response[0][0].to_i
161
- md5_hash = listfiles_response[0][1]
162
- size = listfiles_response[0][2].to_i
163
- already_exists = true
164
- end
165
-
166
- raise Exception, "Invalid File ID: #{resp.inspect}" unless id
167
- raise Exception, "Invalid MD5 hash: #{resp.inspect}" unless md5_hash
168
- raise Exception, "Invalid File Size: #{resp.inspect}" unless size
169
-
170
- {
171
- :id => id,
172
- :size => size,
173
- :checksum => md5_hash.downcase,
174
- :url => "https://rapidshare.com/files/#{id}/#{URI::encode file_name}",
175
- :already_exists? => already_exists
176
- }
177
- end
178
-
179
- # Delete file
180
- #
181
- # @param [String] path
182
- # @param [Hash] params
183
- #
184
- # api.remove_file("/putin/is/a/good/reason/to/live/abroad/ticket_to_Nikaragua.jpg")
185
- def remove_file(path, params = {})
186
- params = {
187
- :files => file_id(path).to_s
188
- }.merge params
189
-
190
- deletefiles params
191
- end
192
-
193
- # Rename file
194
- #
195
- # @param [String] remote_path
196
- # @param [String] name
197
- # @param [Hash] params
198
- #
199
- # api.rename_file("/foo/bar.rar", "baz.rar")
200
- def rename_file(remote_path, name, params = {})
201
- file_id = file_id remote_path
202
-
203
- params = {
204
- :fileid => file_id,
205
- :newname => name
206
- }.merge params
207
-
208
- renamefile params
209
- # TODO: duplicates check
210
- end
211
-
212
- # Moves file to a specified folder
213
- #
214
- # @param [String] remote_path
215
- # @param [Hash] params
216
- # <tt>:to</tt>::
217
- # Destination folder path, default: "/"
218
- #
219
- # api.move_file("/foo/bar/baz.rar", :to => "/foo")
220
- # api.move_file("/foo/bar/baz.rar") # move to a root folder
221
- def move_file(remote_path, params = {})
222
- file_id = file_id remote_path
223
- dest_path = path_trim(params.delete(:to) || '/')
224
-
225
- params = {
226
- :files => file_id,
227
- :realfolder => folder_id(dest_path)
228
- }.merge params
229
-
230
- movefilestorealfolder params
231
- end
232
-
233
- # See #folders_hierarchy method
234
- def folders_hierarchy!(params = {})
235
- params[:force] = true
236
- folders_hierarchy params
237
- end
238
-
239
- alias :reload! :folders_hierarchy!
240
-
241
- # Build folders hierarchy in the following format:
242
- # {
243
- # <folder ID> => {
244
- # :parent => <parent folder ID>,
245
- # :name => <folder name>,
246
- # :path => <folder absolute path>
247
- # },
248
- # ...
249
- # }
250
- #
251
- # @param [Hash] params
252
- # <tt>:force</tt>::
253
- # Invalidate cached tree, default: false
254
- # After each call of this method the generated tree will be saved as cache
255
- # to avoid unnecessary queries to be performed fpr a future calls
256
- # <tt>:validate</tt>::
257
- # Validate tree after it has been generated, default: true
258
- # <tt>:consistent</tt>::
259
- # Delete all found orphans, default: false
260
- # Ignored if :validate is set to false
261
- def folders_hierarchy(params = {})
262
- force_load = params.delete :force
263
- from_folder_path = path_trim(params.delete(:from) || '/')
264
- remove_orphans = params.delete(:consistent)
265
- perform_validation = params.delete(:validate)
266
- perform_validation = true if perform_validation.nil?
267
- remove_orphans = false unless perform_validation
268
-
269
- if @tree && !force_load
270
- if from_folder_path.empty?
271
- return @tree
272
- else
273
- return slice_tree @tree, :from => from_folder_path
274
- end
275
- end
276
-
277
- return @tree if @tree && !force_load # TODO: about slices here (:from parameter)
278
- @tree = {}
279
-
280
- from_folder_id = folder_id from_folder_path
281
- raise Exception, "Folder #{from_folder_path} could not be found" if from_folder_id.nil?
282
-
283
- response = listrealfolders
284
-
285
- if 'NONE' == response
286
- @tree = {}
287
- else
288
- intermediate = response.split(' ').map do |str|
289
- params = str.split ','
290
- [params[0].to_i, {:parent => params[1].to_i, :name => params[2]}]
291
- end
292
-
293
- @tree = Hash[intermediate]
294
- end
295
-
296
- # Kill orphans
297
- remove_orphans! if remove_orphans
298
-
299
- @tree.each_pair do |folder_id, data|
300
- @tree[folder_id][:path] = folder_path folder_id
301
- end
302
-
303
- if perform_validation
304
- # Validate folder tree consistency
305
- @tree.each_pair do |folder_id, data|
306
- parent_id = data[:parent]
307
- if !parent_id.zero? && @tree[parent_id].nil?
308
- error = "Directory tree consistency error. Parent folder ##{data[:parent]} for the folder \"#{data[:path]}\" [#{folder_id}] could not be found"
309
- raise error
310
- end
311
- end
312
- end
313
-
314
- @tree = slice_tree @tree, :from => from_folder_path unless from_folder_path.empty?
315
- @tree
316
- end
317
-
318
- # Build tree relative to a specified folder
319
- # If the source tree is:
320
- # tree = {
321
- # 1 => {:parent => 0, :name => "a", :path => "a"},
322
- # 2 => {:parent => 1, :name => "b", :path => "a/b"},
323
- # 3 => {:parent => 2, :name => "c", :path => "a/b/c"},
324
- # ...
325
- # }
326
- # slice_tree tree, :from => "/a"
327
- # Result will be as follows:
328
- # {
329
- # 2 => {:parent => 1, :name => "b", :path => "b"},
330
- # 3 => {:parent => 2, :name => "c", :path => "b/c"},
331
- # ...
332
- # }
333
- def slice_tree(tree, params = {})
334
- from_folder_path = path_trim(params.delete(:from) || '/')
335
-
336
- result_tree = tree.dup
337
-
338
- unless from_folder_path == ''
339
-
340
- result_tree.keep_if do |folder_id, data|
341
- path_trim(data[:path]).start_with? "#{from_folder_path}/"
342
- end
343
-
344
- result_tree.each_pair do |folder_id, data|
345
- path = result_tree[folder_id][:path]
346
- result_tree[folder_id][:path] = path_canonize path_trim(path.gsub /#{from_folder_path.gsub /\//, '\/'}\//, '')
347
- end
348
- end
349
-
350
- result_tree
351
- end
352
-
353
- # Fix inconsistent folder tree (Yes, getting a broken folder hierarchy is possible with a stupid Rapidshare API)
354
- # by deleting orphan folders (folders with no parent folder), this folders are invisible in Rapidshare File Manager
355
- # So, this method deletes orphan folders
356
- def remove_orphans!
357
- @tree = folders_hierarchy :validate => false
358
- @tree.each_pair do |folder_id, data|
359
- @tree.delete_if do |folder_id, data|
360
- if orphan? folder_id
361
- delrealfolder :realfolder => folder_id
362
- true
363
- end
364
- end
365
- end
366
- end
367
-
368
- # Places all existing orphan folders under the specific folder
369
- # Orphan folder is a folder with non existing parent (yes, it's possible)
370
- #
371
- # Example:
372
- # move_orphans :to => "/"
373
- def move_orphans(params = {})
374
- new_folder = path_trim(params.delete(:to) || '/')
375
- gaps = detect_gaps
376
-
377
- if gaps.any?
378
- params = {
379
- :realfolder => gaps.join(','),
380
- :newparent => new_folder
381
- }.merge params
382
- moverealfolder params
383
- end
384
- end
385
-
386
- # Returns gap list between folders
387
- # See #gap? for example
388
- def detect_gaps
389
- @tree = folders_hierarchy :validate => false
390
- @tree.dup.keep_if do |folder_id, data|
391
- gap? folder_id # This is wrong
392
- end.keys
393
- end
394
-
395
- # The name speaks for itself
396
- # WARNING!!! All data will be lost!!!
397
- # Use it carefully
398
- def erase_all_data!
399
- @tree = folders_hierarchy! :validate => false
400
- @tree.keys.each do |folder_id|
401
- delrealfolder :realfolder => folder_id
402
- end
403
- folders_hierarchy!
404
- end
405
-
406
- # Check if folder with given id placed on the bottom of folder hierarchy
407
- def root_folder?(folder_id)
408
- @tree = folders_hierarchy :validate => false
409
- return false if @tree[folder_id].nil?
410
- @tree[folder_id][:parent].zero?
411
- end
412
-
413
- # Check if the given folder has no parent
414
- def gap?(folder_id)
415
- @tree = folders_hierarchy :validate => false
416
- parent_id = @tree[folder_id][:parent]
417
- @tree[parent_id].nil?
418
- end
419
-
420
- # Check if folder has any gaps in it hierarchy
421
- # For example we have the following hierarchy:
422
- #
423
- # ROOT
424
- # `-a <- if we remove just this folder then the folder "c" and "b" will become orphans
425
- # `-b
426
- # `-c
427
- def orphan?(folder_id)
428
- @tree = folders_hierarchy :validate => false
429
- return false if @tree[folder_id].nil?
430
- parent_id = @tree[folder_id][:parent]
431
- return false if root_folder? folder_id
432
- return true if gap? folder_id
433
- orphan?(parent_id)
434
- end
435
-
436
- # Translate folder ID to a human readable path
437
- #
438
- # api.folder_path(123) # -> "foo/bar/baz"
439
- def folder_path(folder_id)
440
- @tree = folders_hierarchy
441
-
442
- folder_data = @tree[folder_id] || {:parent => 0, :name => "<undefined>", :path => "<undefined>"}
443
-
444
- parent_id = folder_data[:parent]
445
- path = (folder_path(parent_id) if parent_id.nonzero?).to_s + ('/' if parent_id.nonzero?).to_s + folder_data[:name]
446
- parent_id.zero? ? "/#{path}" : path
447
- end
448
-
449
- # Get folder ID by path
450
- #
451
- # api.folder_id("foo/bar/baz") # -> 123
452
- def folder_id(folder_path)
453
- folder_path = path_trim(folder_path)
454
- return 0 if folder_path.empty?
455
-
456
- @tree = folders_hierarchy
457
- index = @tree.find_index do |folder_id, data|
458
- path_trim(data[:path]) == path_trim(folder_path)
459
- end
460
- @tree.keys[index] unless index.nil?
461
- end
462
-
463
- # Get file info in the following format:
464
- #
465
- # {
466
- # :downloads,
467
- # :lastdownload,
468
- # :filename,
469
- # :size,
470
- # :serverid,
471
- # :type,
472
- # :x,
473
- # :y,
474
- # :realfolder,
475
- # :killdeadline,
476
- # :uploadtime,
477
- # :comment,
478
- # :md5hex,
479
- # :licids,
480
- # :sentby
481
- # }
482
- # See the http://images.rapidshare.com/apidoc.txt for more details
483
- def file_info(file_path, params = {})
484
- folder_path = File.dirname file_path
485
- file_name = File.basename file_path
486
-
487
- folder_id = folder_id folder_path
488
-
489
- listfiles_params = {
490
- :realfolder => folder_id,
491
- :filename => "#{file_name}",
492
- :fields => FILE_COLUMNS,
493
- :parser => :csv
494
- }.merge params
495
-
496
- resp = listfiles(listfiles_params)[0]
497
- return nil if "NONE" == resp[0]
498
-
499
- response = {}
500
-
501
- fields = listfiles_params[:fields].split(',')
502
- fields.unshift "id"
503
- fields.each_with_index do |value, index|
504
- response[value.to_sym] = resp[index]
505
- end
506
-
507
- response[:url] = "https://rapidshare.com/files/#{response[:id]}/#{URI::encode response[:filename]}" if response[:filename]
508
-
509
- response
510
- end
511
-
512
- # Returns file ID by absolute path
513
- #
514
- # api.file_id("foo/bar/baz/file.rar") # => <FILE_ID>
515
- def file_id(file_path, params = {})
516
- params[:fields] = ""
517
- file_info = file_info file_path, params
518
- (file_info || {})[:id].to_i
519
- end
520
-
521
- protected
522
-
523
- def path_trim(path)
524
- path.gsub(/\A\/+/, '').gsub(/\/+\Z/, '')
525
- end
526
-
527
- def path_canonize(path)
528
- "/" + path_trim(path)
529
- end
530
- end
531
- end
1
+ module Rapidshare
2
+ module Ext
3
+ module API
4
+
5
+ FILE_COLUMNS = 'downloads,lastdownload,filename,size,serverid,type,x,y,realfolder,killdeadline,uploadtime,comment,md5hex,licids,sentby'
6
+
7
+ # @param [String] path Folder name with absolute path to be created
8
+ # @param [Hash] params
9
+ # @return [Integer]
10
+ #
11
+ # Creates a folder in a Rapidshare virtual filesystem
12
+ #
13
+ # api.add_folder('/a/b/c') #=> <Random folder ID from Rapidshare>, 1234 for example
14
+ def add_folder(path, params = {})
15
+ path = path_trim path
16
+
17
+ @tree = folders_hierarchy
18
+ i = 1
19
+ parent = 0
20
+ folder_id = nil
21
+ while i <= path.split('/').count do
22
+ base_path = path.split('/')[0,i].join('/')
23
+ folder_id = self.folder_id base_path
24
+ if folder_id
25
+ parent = folder_id
26
+ i += 1
27
+ else
28
+ # Create folder
29
+ folder_name = path.split('/')[i-1]
30
+ add_folder_params = {
31
+ :name => folder_name,
32
+ :parent => parent
33
+ }.merge params
34
+
35
+ # The following code deals with #{} because of rest client #to_i returns HTTP code
36
+ folder_id = "#{addrealfolder(add_folder_params)}".to_i
37
+ raise 'error while creating folder' if parent < 0
38
+ @tree[folder_id] = {
39
+ :parent => parent,
40
+ :name => folder_name,
41
+ :path => path_canonize((@tree[parent] || {})[:path].to_s + ('/' if @tree[parent]).to_s + folder_name)
42
+ }
43
+ parent = folder_id
44
+ path == base_path + '/' + folder_name
45
+ i += 1
46
+ next
47
+ end
48
+ end
49
+ folder_id
50
+ end
51
+
52
+ # Removes a specified folder
53
+ #
54
+ # @param [String] path
55
+ # @param [Hash] params
56
+ # @return [Array]
57
+ #
58
+ # api.remove_folder('/a/b/c')
59
+ def remove_folder(path, params = {})
60
+ folder_id = self.folder_id path_trim(path)
61
+ raise Exception, "Folder #{path} could not be found" if folder_id.nil?
62
+
63
+ # TODO
64
+ tree = folders_hierarchy :from => path
65
+ tree.each_pair do |child_folder_id, data|
66
+ delrealfolder_params = {
67
+ :realfolder => child_folder_id
68
+ }.merge params
69
+
70
+ delrealfolder delrealfolder_params
71
+ @tree.delete folder_id
72
+ end
73
+
74
+ params = {
75
+ :realfolder => folder_id
76
+ }.merge params
77
+
78
+ delrealfolder params
79
+
80
+ @tree.delete folder_id
81
+ end
82
+
83
+ # Moves folder into a specified one
84
+ #
85
+ # @param [String] source_path
86
+ # @param [Hash] params
87
+ # :to => <destination folder path>, default: '/'
88
+ #
89
+ # api.move_folder('/a/b/c', :to => '/a')
90
+ def move_folder(source_path, params = {})
91
+ dest_path = (params.delete(:to) || '/')
92
+ source_folder_id = folder_id(source_path)
93
+ dest_folder_id = folder_id(dest_path)
94
+
95
+ params = {
96
+ :realfolder => source_folder_id,
97
+ :newparent => dest_folder_id
98
+ }.merge params
99
+
100
+ moverealfolder params
101
+
102
+ @tree = folders_hierarchy
103
+ @tree[source_folder_id][:parent] = dest_folder_id
104
+ @tree[source_folder_id][:path] = path_canonize "#{folder_path(dest_folder_id)}/#{@tree[source_folder_id][:name]}"
105
+ true
106
+ end
107
+
108
+ # Upload file to a specified folder
109
+ #
110
+ # @param [String] file_path
111
+ # @param [Hash] params
112
+ # <tt>:to</tt>::
113
+ # Folder to place uploaded file to, default: '/'
114
+ # <tt>:as</tt>::
115
+ # The name file will have in storage after it has been uploaded
116
+ # <tt>:overwrite</tt>::
117
+ # Overwrite file if it already exists in the given folder
118
+ #
119
+ # api.upload('/home/odiszapc/my_damn_cat.mov', :to => '/gallery/video', :as => 'cat1.mov')
120
+ def upload(file_path, params = {})
121
+ raise Exception unless File.exist? file_path
122
+ dest_path = path_trim(params.delete(:to) || '/')
123
+ folder_id = self.add_folder dest_path
124
+ file_name = params.delete(:as) || File.basename(file_path)
125
+ overwrite = params.delete :overwrite
126
+
127
+ # Check file already exists within a folder
128
+ listfiles_params = {
129
+ :realfolder => folder_id,
130
+ :filename => "#{file_name}",
131
+ :fields => 'md5hex,size',
132
+ :parser => :csv
133
+ }
134
+ listfiles_response = self.listfiles listfiles_params
135
+
136
+ file_already_exists = ('NONE' != listfiles_response[0][0])
137
+ remove_file "#{dest_path}/#{file_name}" if file_already_exists && overwrite
138
+
139
+ # In case of file is not existing then upload it
140
+ if !file_already_exists || overwrite
141
+ upload_server = "rs#{self.nextuploadserver}.rapidshare.com"
142
+
143
+ upload_params = {
144
+ :server => upload_server,
145
+ :folder => folder_id,
146
+ :filename => file_name,
147
+ :filecontent => file_path,
148
+ :method => :post,
149
+ :parser => :csv
150
+ }.merge params
151
+
152
+ resp = request(:upload, upload_params)
153
+ raise Exception, "File uploading failed: #{resp.inspect}" unless "COMPLETE" == resp[0][0]
154
+
155
+ id = resp[1][0].to_i
156
+ md5_hash = resp[1][3]
157
+ size = resp[1][2].to_i
158
+ already_exists = false
159
+ else
160
+ id = listfiles_response[0][0].to_i
161
+ md5_hash = listfiles_response[0][1]
162
+ size = listfiles_response[0][2].to_i
163
+ already_exists = true
164
+ end
165
+
166
+ raise Exception, "Invalid File ID: #{resp.inspect}" unless id
167
+ raise Exception, "Invalid MD5 hash: #{resp.inspect}" unless md5_hash
168
+ raise Exception, "Invalid File Size: #{resp.inspect}" unless size
169
+
170
+ {
171
+ :id => id,
172
+ :size => size,
173
+ :checksum => md5_hash.downcase,
174
+ :url => "https://rapidshare.com/files/#{id}/#{URI::encode file_name}",
175
+ :already_exists? => already_exists
176
+ }
177
+ end
178
+
179
+ # Delete file
180
+ #
181
+ # @param [String] path
182
+ # @param [Hash] params
183
+ #
184
+ # api.remove_file('/putin/is/a/good/reason/to/live/abroad/ticket_to_Nikaragua.jpg')
185
+ def remove_file(path, params = {})
186
+ params = {
187
+ :files => file_id(path).to_s
188
+ }.merge params
189
+
190
+ deletefiles params
191
+ end
192
+
193
+ # Rename file
194
+ #
195
+ # @param [String] remote_path
196
+ # @param [String] name
197
+ # @param [Hash] params
198
+ #
199
+ # api.rename_file('/foo/bar.rar', 'baz.rar')
200
+ def rename_file(remote_path, name, params = {})
201
+ file_id = file_id remote_path
202
+
203
+ params = {
204
+ :fileid => file_id,
205
+ :newname => name
206
+ }.merge params
207
+
208
+ renamefile params
209
+ # TODO: duplicates check
210
+ end
211
+
212
+ # Moves file to a specified folder
213
+ #
214
+ # @param [String] remote_path
215
+ # @param [Hash] params
216
+ # <tt>:to</tt>::
217
+ # Destination folder path, default: '/'
218
+ #
219
+ # api.move_file('/foo/bar/baz.rar', :to => '/foo')
220
+ # api.move_file('/foo/bar/baz.rar') # move to a root folder
221
+ def move_file(remote_path, params = {})
222
+ file_id = file_id remote_path
223
+ dest_path = path_trim(params.delete(:to) || '/')
224
+
225
+ params = {
226
+ :files => file_id,
227
+ :realfolder => folder_id(dest_path)
228
+ }.merge params
229
+
230
+ movefilestorealfolder params
231
+ end
232
+
233
+ # See #folders_hierarchy method
234
+ def folders_hierarchy!(params = {})
235
+ params[:force] = true
236
+ folders_hierarchy params
237
+ end
238
+
239
+ alias :reload! :folders_hierarchy!
240
+
241
+ # Build folders hierarchy in the following format:
242
+ # {
243
+ # <folder ID> => {
244
+ # :parent => <parent folder ID>,
245
+ # :name => <folder name>,
246
+ # :path => <folder absolute path>
247
+ # },
248
+ # ...
249
+ # }
250
+ #
251
+ # @param [Hash] params
252
+ # <tt>:force</tt>::
253
+ # Invalidate cached tree, default: false
254
+ # After each call of this method the generated tree will be saved as cache
255
+ # to avoid unnecessary queries to be performed fpr a future calls
256
+ # <tt>:validate</tt>::
257
+ # Validate tree after it has been generated, default: true
258
+ # <tt>:consistent</tt>::
259
+ # Delete all found orphans, default: false
260
+ # Ignored if :validate is set to false
261
+ def folders_hierarchy(params = {})
262
+ force_load = params.delete :force
263
+ from_folder_path = path_trim(params.delete(:from) || '/')
264
+ remove_orphans = params.delete(:consistent)
265
+ perform_validation = params.delete(:validate)
266
+ perform_validation = true if perform_validation.nil?
267
+ remove_orphans = false unless perform_validation
268
+
269
+ if @tree && !force_load
270
+ if from_folder_path.empty?
271
+ return @tree
272
+ else
273
+ return slice_tree @tree, :from => from_folder_path
274
+ end
275
+ end
276
+
277
+ return @tree if @tree && !force_load # TODO: about slices here (:from parameter)
278
+ @tree = {}
279
+
280
+ from_folder_id = folder_id from_folder_path
281
+ raise Exception, "Folder #{from_folder_path} could not be found" if from_folder_id.nil?
282
+
283
+ response = listrealfolders
284
+
285
+ if 'NONE' == response
286
+ @tree = {}
287
+ else
288
+ intermediate = response.split(' ').map do |str|
289
+ params = str.split ','
290
+ [params[0].to_i, {:parent => params[1].to_i, :name => params[2]}]
291
+ end
292
+
293
+ @tree = Hash[intermediate]
294
+ end
295
+
296
+ # Kill orphans
297
+ remove_orphans! if remove_orphans
298
+
299
+ @tree.each_pair do |folder_id, data|
300
+ @tree[folder_id][:path] = folder_path folder_id
301
+ end
302
+
303
+ if perform_validation
304
+ # Validate folder tree consistency
305
+ @tree.each_pair do |folder_id, data|
306
+ parent_id = data[:parent]
307
+ if !parent_id.zero? && @tree[parent_id].nil?
308
+ error = "Directory tree consistency error. Parent folder ##{data[:parent]} for the folder \"#{data[:path]}\" [#{folder_id}] could not be found"
309
+ raise error
310
+ end
311
+ end
312
+ end
313
+
314
+ @tree = slice_tree @tree, :from => from_folder_path unless from_folder_path.empty?
315
+ @tree
316
+ end
317
+
318
+ # Build tree relative to a specified folder
319
+ # If the source tree is:
320
+ # tree = {
321
+ # 1 => {:parent => 0, :name => 'a', :path => 'a'},
322
+ # 2 => {:parent => 1, :name => 'b', :path => 'a/b'},
323
+ # 3 => {:parent => 2, :name => 'c', :path => 'a/b/c'},
324
+ # ...
325
+ # }
326
+ # slice_tree tree, :from => '/a'
327
+ # Result will be as follows:
328
+ # {
329
+ # 2 => {:parent => 1, :name => 'b', :path => 'b'},
330
+ # 3 => {:parent => 2, :name => 'c', :path => 'b/c'},
331
+ # ...
332
+ # }
333
+ def slice_tree(tree, params = {})
334
+ from_folder_path = path_trim(params.delete(:from) || '/')
335
+
336
+ result_tree = tree.dup
337
+
338
+ unless from_folder_path == ''
339
+
340
+ result_tree.keep_if do |folder_id, data|
341
+ path_trim(data[:path]).start_with? "#{from_folder_path}/"
342
+ end
343
+
344
+ result_tree.each_pair do |folder_id, data|
345
+ path = result_tree[folder_id][:path]
346
+ result_tree[folder_id][:path] = path_canonize path_trim(path.gsub /#{from_folder_path.gsub /\//, '\/'}\//, '')
347
+ end
348
+ end
349
+
350
+ result_tree
351
+ end
352
+
353
+ # Fix inconsistent folder tree (Yes, getting a broken folder hierarchy is possible with a stupid Rapidshare API)
354
+ # by deleting orphan folders (folders with no parent folder), this folders are invisible in Rapidshare File Manager
355
+ # So, this method deletes orphan folders
356
+ def remove_orphans!
357
+ @tree = folders_hierarchy :validate => false
358
+ @tree.each_pair do |folder_id, data|
359
+ @tree.delete_if do |folder_id, data|
360
+ if orphan? folder_id
361
+ delrealfolder :realfolder => folder_id
362
+ true
363
+ end
364
+ end
365
+ end
366
+ end
367
+
368
+ # Places all existing orphan folders under the specific folder
369
+ # Orphan folder is a folder with non existing parent (yes, it's possible)
370
+ #
371
+ # Example:
372
+ # move_orphans :to => '/'
373
+ def move_orphans(params = {})
374
+ new_folder = path_trim(params.delete(:to) || '/')
375
+ gaps = detect_gaps
376
+
377
+ if gaps.any?
378
+ params = {
379
+ :realfolder => gaps.join(','),
380
+ :newparent => new_folder
381
+ }.merge params
382
+ moverealfolder params
383
+ end
384
+ end
385
+
386
+ # Returns gap list between folders
387
+ # See #gap? for example
388
+ def detect_gaps
389
+ @tree = folders_hierarchy :validate => false
390
+ @tree.dup.keep_if do |folder_id, data|
391
+ gap? folder_id # This is wrong
392
+ end.keys
393
+ end
394
+
395
+ # The name speaks for itself
396
+ # WARNING!!! All data will be lost!!!
397
+ # Use it carefully
398
+ def erase_all_data!
399
+ @tree = folders_hierarchy! :validate => false
400
+ @tree.keys.each do |folder_id|
401
+ delrealfolder :realfolder => folder_id
402
+ end
403
+ folders_hierarchy!
404
+ end
405
+
406
+ # Check if folder with given id placed on the bottom of folder hierarchy
407
+ def root_folder?(folder_id)
408
+ @tree = folders_hierarchy :validate => false
409
+ return false if @tree[folder_id].nil?
410
+ @tree[folder_id][:parent].zero?
411
+ end
412
+
413
+ # Check if the given folder has no parent
414
+ def gap?(folder_id)
415
+ @tree = folders_hierarchy :validate => false
416
+ parent_id = @tree[folder_id][:parent]
417
+ @tree[parent_id].nil?
418
+ end
419
+
420
+ # Check if folder has any gaps in it hierarchy
421
+ # For example we have the following hierarchy:
422
+ #
423
+ # ROOT
424
+ # `-a <- if we remove just this folder then the folder 'c' and 'b' will become orphans
425
+ # `-b
426
+ # `-c
427
+ def orphan?(folder_id)
428
+ @tree = folders_hierarchy :validate => false
429
+ return false if @tree[folder_id].nil?
430
+ parent_id = @tree[folder_id][:parent]
431
+ return false if root_folder? folder_id
432
+ return true if gap? folder_id
433
+ orphan?(parent_id)
434
+ end
435
+
436
+ # Translate folder ID to a human readable path
437
+ #
438
+ # api.folder_path(123) # -> 'foo/bar/baz'
439
+ def folder_path(folder_id)
440
+ @tree = folders_hierarchy
441
+
442
+ folder_data = @tree[folder_id] || {:parent => 0, :name => '<undefined>', :path => '<undefined>'}
443
+
444
+ parent_id = folder_data[:parent]
445
+ path = (folder_path(parent_id) if parent_id.nonzero?).to_s + ('/' if parent_id.nonzero?).to_s + folder_data[:name]
446
+ parent_id.zero? ? "/#{path}" : path
447
+ end
448
+
449
+ # Get folder ID by path
450
+ #
451
+ # api.folder_id('foo/bar/baz') # -> 123
452
+ def folder_id(folder_path)
453
+ folder_path = path_trim(folder_path)
454
+ return 0 if folder_path.empty?
455
+
456
+ @tree = folders_hierarchy
457
+ index = @tree.find_index do |folder_id, data|
458
+ path_trim(data[:path]) == path_trim(folder_path)
459
+ end
460
+ @tree.keys[index] unless index.nil?
461
+ end
462
+
463
+ # Get file info in the following format:
464
+ #
465
+ # {
466
+ # :downloads,
467
+ # :lastdownload,
468
+ # :filename,
469
+ # :size,
470
+ # :serverid,
471
+ # :type,
472
+ # :x,
473
+ # :y,
474
+ # :realfolder,
475
+ # :killdeadline,
476
+ # :uploadtime,
477
+ # :comment,
478
+ # :md5hex,
479
+ # :licids,
480
+ # :sentby
481
+ # }
482
+ # See the http://images.rapidshare.com/apidoc.txt for more details
483
+ def file_info(file_path, params = {})
484
+ folder_path = File.dirname file_path
485
+ file_name = File.basename file_path
486
+
487
+ folder_id = folder_id folder_path
488
+
489
+ listfiles_params = {
490
+ :realfolder => folder_id,
491
+ :filename => "#{file_name}",
492
+ :fields => FILE_COLUMNS,
493
+ :parser => :csv
494
+ }.merge params
495
+
496
+ resp = listfiles(listfiles_params)[0]
497
+ return nil if 'NONE' == resp[0]
498
+
499
+ response = {}
500
+
501
+ fields = listfiles_params[:fields].split(',')
502
+ fields.unshift 'id'
503
+ fields.each_with_index do |value, index|
504
+ response[value.to_sym] = resp[index]
505
+ end
506
+
507
+ response[:url] = "https://rapidshare.com/files/#{response[:id]}/#{URI::encode response[:filename]}" if response[:filename]
508
+
509
+ response
510
+ end
511
+
512
+ # Returns file ID by absolute path
513
+ #
514
+ # api.file_id('foo/bar/baz/file.rar') # => <FILE_ID>
515
+ def file_id(file_path, params = {})
516
+ params[:fields] = ''
517
+ file_info = file_info file_path, params
518
+ (file_info || {})[:id].to_i
519
+ end
520
+
521
+ protected
522
+
523
+ def path_trim(path)
524
+ path.gsub(/\A\/+/, '').gsub(/\/+\Z/, '')
525
+ end
526
+
527
+ def path_canonize(path)
528
+ '/' + path_trim(path)
529
+ end
530
+ end
531
+ end
532
532
  end