rapidshare-ext 0.0.6 → 0.1.0

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