condo 1.0.6 → 2.0.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.textile +19 -32
  3. data/lib/condo.rb +124 -127
  4. data/lib/condo/configuration.rb +41 -76
  5. data/lib/condo/engine.rb +32 -39
  6. data/lib/condo/errors.rb +6 -8
  7. data/lib/condo/strata/amazon_s3.rb +246 -294
  8. data/lib/condo/strata/google_cloud_storage.rb +238 -272
  9. data/lib/condo/strata/open_stack_swift.rb +251 -0
  10. data/lib/condo/version.rb +1 -1
  11. metadata +31 -96
  12. data/app/assets/javascripts/condo.js +0 -9
  13. data/app/assets/javascripts/condo/amazon.js +0 -403
  14. data/app/assets/javascripts/condo/condo.js +0 -184
  15. data/app/assets/javascripts/condo/config.js +0 -69
  16. data/app/assets/javascripts/condo/google.js +0 -338
  17. data/app/assets/javascripts/condo/md5/hash.worker.emulator.js +0 -23
  18. data/app/assets/javascripts/condo/md5/hash.worker.js +0 -11
  19. data/app/assets/javascripts/condo/md5/hasher.js +0 -119
  20. data/app/assets/javascripts/condo/md5/spark-md5.js +0 -599
  21. data/app/assets/javascripts/condo/rackspace.js +0 -326
  22. data/app/assets/javascripts/condo/services/abstract-md5.js.erb +0 -86
  23. data/app/assets/javascripts/condo/services/base64.js +0 -184
  24. data/app/assets/javascripts/condo/services/broadcaster.js +0 -26
  25. data/app/assets/javascripts/condo/services/uploader.js +0 -302
  26. data/app/assets/javascripts/core/core.js +0 -4
  27. data/app/assets/javascripts/core/services/1-safe-apply.js +0 -17
  28. data/app/assets/javascripts/core/services/2-messaging.js +0 -171
  29. data/lib/condo/strata/rackspace_cloud_files.rb +0 -245
  30. data/test/condo_test.rb +0 -27
  31. data/test/dummy/README.rdoc +0 -261
  32. data/test/dummy/Rakefile +0 -7
  33. data/test/dummy/app/assets/javascripts/application.js +0 -15
  34. data/test/dummy/app/assets/stylesheets/application.css +0 -13
  35. data/test/dummy/app/controllers/application_controller.rb +0 -3
  36. data/test/dummy/app/helpers/application_helper.rb +0 -2
  37. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  38. data/test/dummy/config.ru +0 -4
  39. data/test/dummy/config/application.rb +0 -59
  40. data/test/dummy/config/boot.rb +0 -10
  41. data/test/dummy/config/database.yml +0 -25
  42. data/test/dummy/config/environment.rb +0 -5
  43. data/test/dummy/config/environments/development.rb +0 -37
  44. data/test/dummy/config/environments/production.rb +0 -67
  45. data/test/dummy/config/environments/test.rb +0 -37
  46. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  47. data/test/dummy/config/initializers/inflections.rb +0 -15
  48. data/test/dummy/config/initializers/mime_types.rb +0 -5
  49. data/test/dummy/config/initializers/secret_token.rb +0 -7
  50. data/test/dummy/config/initializers/session_store.rb +0 -8
  51. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  52. data/test/dummy/config/locales/en.yml +0 -5
  53. data/test/dummy/config/routes.rb +0 -58
  54. data/test/dummy/public/404.html +0 -26
  55. data/test/dummy/public/422.html +0 -26
  56. data/test/dummy/public/500.html +0 -25
  57. data/test/dummy/public/favicon.ico +0 -0
  58. data/test/dummy/script/rails +0 -6
  59. data/test/integration/navigation_test.rb +0 -10
  60. data/test/test_helper.rb +0 -15
@@ -1,12 +1,16 @@
1
1
  require 'singleton'
2
2
 
3
+
3
4
  module Condo
4
-
5
+
5
6
  class Configuration
6
7
  include Singleton
7
-
8
+
9
+
8
10
  @@callbacks = {
11
+ #
9
12
  #:resident_id # Must be defined by the including class
13
+ #
10
14
  :bucket_name => proc {"#{Rails.application.class.parent_name}#{instance_eval @@callbacks[:resident_id]}"},
11
15
  :object_key => proc { |upload|
12
16
  if upload[:file_path]
@@ -22,25 +26,31 @@ module Condo
22
26
  true
23
27
  }, # To respond with errors use: lambda {return false, {:errors => {:param_name => 'wtf are you doing?'}}}
24
28
  :sanitize_filename => proc { |filename|
29
+ filename = filename.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
25
30
  filename.gsub!(/^.*(\\|\/)/, '') # get only the filename (just in case)
26
- filename.gsub!(/[^\w\.\-]/, '_') # replace all non alphanumeric or periods with underscore
31
+ filename.gsub!(/[^\w\.\-]/, '_') # replace all non alphanumeric or periods with underscore
27
32
  filename
28
33
  },
29
34
  :sanitize_filepath => proc { |filepath|
35
+ filepath = filepath.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
30
36
  filepath.gsub!(/[^\w\.\-\/]/, '_') # replace all non alphanumeric or periods with underscore
31
37
  filepath
38
+ },
39
+ :select_residence => proc { |config, resident_id, upload|
40
+ # Where config === ::Condo::Configuration
41
+ # and resident_id is the result of the resident_id callback
42
+ # upload will only be present if it already exists
43
+ config.residencies[0]
32
44
  }
33
45
  #:upload_complete # Must be defined by the including class
34
- #:destroy_upload # the actual delete should be done by the application
35
- #:dynamic_residence # If the data stores are dynamically stored by the application
46
+ #:destroy_upload # the actual delete should be done by the application
36
47
  }
37
-
38
- @@dynamics = {}
39
-
48
+
40
49
  def self.callbacks
41
50
  @@callbacks
42
51
  end
43
-
52
+
53
+ # Allows you to override default callback behaviour
44
54
  def self.set_callback(name, callback = nil, &block)
45
55
  callback ||= block
46
56
  if callback.respond_to?(:call)
@@ -49,40 +59,18 @@ module Condo
49
59
  raise ArgumentError, 'No callback provided'
50
60
  end
51
61
  end
52
-
53
-
54
- #
55
- # Provides a callback whenever attempting to select a provider for the current request
56
- # => Allows multiple providers for different users / controllers or dynamic providers
57
- #
58
- def self.set_dynamic_provider(namespace, callback = nil, &block)
59
- callback ||= block
60
- if callback.respond_to?(:call)
61
- @@callbacks[name.to_sym] = callback
62
- else
63
- raise ArgumentError, 'No callback provided'
64
- end
65
- end
66
-
67
- def dynamic_provider_present?(namespace)
68
- return false if @@dynamics.nil? || @@dynamics[namespace.to_sym].nil?
69
- true
70
- end
71
-
72
-
73
- #
62
+
74
63
  # Allows for predefined storage providers (maybe you only use Amazon?)
75
- #
76
64
  def self.add_residence(name, options = {})
77
65
  @@residencies ||= []
78
66
  @@residencies << ("Condo::Strata::#{name.to_s.camelize}".constantize.new(options)).tap do |res|
79
67
  name = name.to_sym
80
68
  namespace = (options[:namespace] || :global).to_sym
81
-
69
+
82
70
  @@locations ||= {}
83
71
  @@locations[namespace] ||= {}
84
72
  @@locations[namespace][name] ||= {}
85
-
73
+
86
74
  if options[:location].present?
87
75
  @@locations[namespace][name][options[:location].to_sym] = res
88
76
  else
@@ -91,48 +79,25 @@ module Condo
91
79
  end
92
80
  end
93
81
  end
94
-
95
-
96
- def residencies
97
- @@residencies
98
- end
99
-
100
-
101
- #
102
- # Storage provider selection routine
103
- # => pass in :dynamic => true with :name and connection options to create a new instance
104
- #
105
- def set_residence(name, options)
106
- if options[:namespace].present? && dynamic_provider_present?(options[:namespace])
107
- if options[:upload].present?
108
- upload = options[:upload]
109
- params = {
110
- :user_id => upload.user_id,
111
- :file_name => upload.file_name,
112
- :file_size => upload.file_size,
113
- :provider_name => upload.provider_name,
114
- :provider_location => upload.provider_location,
115
- :provider_namespace => upload.provider_namespace
116
- }
117
- return instance_exec params, &@@dynamics[upload.provider_namespace]
118
- else
119
- params = {
120
- :user_id => options[:resident],
121
- :file_name => options[:params][:file_name],
122
- :file_size => options[:params][:file_size],
123
- :provider_namespace => options[:namespace]
124
- }
125
- return instance_exec params, &@@dynamics[options[:namespace]]
126
- end
127
- else
128
- if options[:dynamic]
129
- return "Condo::Strata::#{name.to_s.camelize}".constantize.new(options)
130
- else
131
- return options[:location].present? ? @@locations[:global][name.to_sym][options[:location].to_sym] : @@locations[:global][name.to_sym][:default]
132
- end
82
+
83
+ def self.get_residence(name, options = {})
84
+ name = name.to_sym
85
+ namespace = (options[:namespace] || :global).to_sym
86
+ location = (options[:location] || :default).to_sym
87
+
88
+ if @@locations && @@locations[namespace] && @@locations[namespace][name] && @@locations[namespace][name][location]
89
+ return @@locations[namespace][name][location]
133
90
  end
91
+
92
+ nil
93
+ end
94
+
95
+ def self.dynamic_residence(name, options = {})
96
+ return "Condo::Strata::#{name.to_s.camelize}".constantize.new(options)
97
+ end
98
+
99
+ def self.residencies
100
+ @@residencies
134
101
  end
135
-
136
102
  end
137
-
138
- end
103
+ end
@@ -1,41 +1,34 @@
1
+ require 'rails'
2
+
1
3
  module Condo
2
- class Engine < ::Rails::Engine
3
-
4
-
5
- #
6
- # Define the base configuration options
7
- #
8
- #config.before_initialize do |app| # Rails.configuration
9
- # app.config.condo = ActiveSupport::OrderedOptions.new
10
- # app.config.condo.providers = ActiveSupport::OrderedOptions.new
11
- #end
12
-
13
-
14
- config.autoload_paths << File.expand_path("../../../lib", __FILE__)
15
-
16
-
17
- #
18
- # Set the proper error types for Rails and add assets for compilation
19
- #
20
- initializer "condo initializer" do |app|
21
-
22
- config.after_initialize do
23
- Rails.application.config.assets.precompile += ['condo/md5/hash.worker.js', 'condo/md5/hash.worker.emulator.js']
24
-
25
- responses = {
26
- "Condo::Errors::MissingFurniture" => :not_found,
27
- "Condo::Errors::LostTheKeys" => :forbidden,
28
- "Condo::Errors::NotYourPlace" => :unauthorized
29
- }
30
- if rescue_responses = config.action_dispatch.rescue_responses # Rails 3.2+
31
- rescue_responses.update(responses)
32
- else
33
- ActionDispatch::ShowExceptions.rescue_responses.update(responses) # Rails 3.0/3.1
34
- end
35
- end
36
-
37
- end
38
-
39
-
40
- end
4
+ class Engine < ::Rails::Engine
5
+
6
+
7
+ # Define the base configuration options
8
+ #
9
+ #config.before_initialize do |app| # Rails.configuration
10
+ # app.config.condo = ActiveSupport::OrderedOptions.new
11
+ # app.config.condo.providers = ActiveSupport::OrderedOptions.new
12
+ #end
13
+
14
+
15
+ config.autoload_paths << File.expand_path("../../../lib", __FILE__)
16
+
17
+
18
+ # Set the proper error types for Rails and add assets for compilation
19
+ initializer "condo initializer" do |app|
20
+ config.after_initialize do
21
+ responses = {
22
+ "Condo::Errors::MissingFurniture" => :not_found,
23
+ "Condo::Errors::LostTheKeys" => :forbidden,
24
+ "Condo::Errors::NotYourPlace" => :unauthorized
25
+ }
26
+ if rescue_responses = config.action_dispatch.rescue_responses # Rails 3.2+
27
+ rescue_responses.update(responses)
28
+ else
29
+ ActionDispatch::ShowExceptions.rescue_responses.update(responses) # Rails 3.0/3.1
30
+ end
31
+ end
32
+ end
33
+ end
41
34
  end
@@ -1,9 +1,7 @@
1
1
  module Condo
2
-
3
- module Errors
4
- class LostTheKeys < RuntimeError; end # Authentication
5
- class NotYourPlace < RuntimeError; end # Authorisation
6
- class MissingFurniture < RuntimeError; end # File not found
7
- end
8
-
9
- end
2
+ module Errors
3
+ class LostTheKeys < RuntimeError; end # Authentication
4
+ class NotYourPlace < RuntimeError; end # Authorisation
5
+ class MissingFurniture < RuntimeError; end # File not found
6
+ end
7
+ end
@@ -3,299 +3,251 @@ module Condo::Strata; end
3
3
 
4
4
 
5
5
  class Condo::Strata::AmazonS3
6
-
7
- def initialize(options)
8
- @options = {
9
- :name => :AmazonS3,
10
- :location => :'us-east-1',
11
- :fog => {
12
- :provider => :AWS,
13
- :aws_access_key_id => options[:access_id],
14
- :aws_secret_access_key => options[:secret_key],
15
- :region => (options[:location] || 'us-east-1')
16
- }
17
- }.merge!(options)
18
-
19
-
20
- raise ArgumentError, 'Amazon Access ID missing' if @options[:access_id].nil?
21
- raise ArgumentError, 'Amazon Secret Key missing' if @options[:secret_key].nil?
22
-
23
-
24
- @options[:location] = @options[:location].to_sym
25
- @options[:region] = @options[:location] == :'us-east-1' ? 's3.amazonaws.com' : "s3-#{@options[:location]}.amazonaws.com"
26
- end
27
-
28
-
29
- def name
30
- @options[:name]
31
- end
32
-
33
-
34
- def location
35
- @options[:location]
36
- end
37
-
38
-
39
-
40
- #
41
- # Create a signed URL for accessing a private file
42
- #
43
- def get_object(options)
44
- options = {}.merge!(options) # Need to deep copy here
45
- options[:object_options] = {
46
- :expires => 5.minutes.from_now,
47
- :date => Time.now,
48
- :verb => :get, # Post for multi-part uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html
49
- :headers => {},
50
- :parameters => {},
51
- :protocol => :https
52
- }.merge!(options[:object_options] || {})
53
- options.merge!(@options)
54
-
55
- #
56
- # provide the signed request
57
- #
58
- sign_request(options)[:url]
59
- end
60
-
61
-
62
- #
63
- # Creates a new upload request (either single shot or multi-part)
64
- # => Passed: bucket_name, object_key, object_options, file_size
65
- #
66
- def new_upload(options)
67
- options = {}.merge!(options) # Need to deep copy here
68
- options[:object_options] = {
69
- :permissions => :private,
70
- :expires => 5.minutes.from_now,
71
- :date => Time.now,
72
- :verb => :post, # Post for multi-part uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html
73
- :headers => {},
74
- :parameters => {},
75
- :protocol => :https
76
- }.merge!(options[:object_options])
77
- options.merge!(@options)
78
-
79
- #
80
- # Set the access control headers
81
- #
82
- if options[:object_options][:headers]['x-amz-acl'].nil?
83
- options[:object_options][:headers]['x-amz-acl'] = case options[:object_options][:permissions]
84
- when :public
85
- :'public-read'
86
- else
87
- :private
88
- end
89
- end
90
-
91
- #
92
- # Decide what type of request is being sent
93
- #
94
- request = {}
95
- if options[:file_size] > 5.megabytes # 5 mb (minimum chunk size)
96
- options[:object_options][:parameters][:uploads] = '' # Customise the request to be a chunked upload
97
- options.delete(:file_id) # Does not apply to chunked uploads
98
-
99
- request[:type] = :chunked_upload
100
- else
101
- if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil?
102
- #
103
- # The client side is sending hex formatted ids that will match the amazon etag
104
- # => We need this to be base64 for the md5 header (this is now done at the client side)
105
- #
106
- # options[:file_id] = [[options[:file_id]].pack("H*")].pack("m0") # (the 0 avoids the call to strip - now done client side)
107
- # [ options[:file_id] ].pack('m').strip # This wasn't correct
108
- # Base64.encode64(options[:file_id]).strip # This also wasn't correct
109
- #
110
- options[:object_options][:headers]['Content-Md5'] = options[:file_id]
111
- end
112
- options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil?
113
- options[:object_options][:verb] = :put # Put for direct uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectPUT.html
114
-
115
- request[:type] = :direct_upload
116
- end
117
-
118
-
119
- #
120
- # provide the signed request
121
- #
122
- request[:signature] = sign_request(options)
123
- request
124
- end
125
-
126
-
127
- #
128
- # Returns the request to get the parts of a resumable upload
129
- #
130
- def get_parts(options)
131
- options[:object_options] = {
132
- :expires => 5.minutes.from_now,
133
- :date => Time.now,
134
- :verb => :get,
135
- :headers => {},
136
- :parameters => {},
137
- :protocol => :https
138
- }.merge!(options[:object_options])
139
- options.merge!(@options)
140
-
141
- #
142
- # Set the upload
143
- #
144
- options[:object_options][:parameters]['uploadId'] = options[:resumable_id]
145
-
146
- #
147
- # provide the signed request
148
- #
149
- {
150
- :type => :parts,
151
- :signature => sign_request(options)
152
- }
153
- end
154
-
155
-
156
- #
157
- # Returns the requests for uploading parts and completing a resumable upload
158
- #
159
- def set_part(options)
160
- options[:object_options] = {
161
- :expires => 5.minutes.from_now,
162
- :date => Time.now,
163
- :headers => {},
164
- :parameters => {},
165
- :protocol => :https
166
- }.merge!(options[:object_options])
167
- options.merge!(@options)
168
-
169
-
170
- request = {}
171
- if options[:part] == 'finish'
172
- #
173
- # Send the commitment response
174
- #
175
- options[:object_options][:headers]['Content-Type'] = 'application/xml; charset=UTF-8' if options[:object_options][:headers]['Content-Type'].nil?
176
- options[:object_options][:verb] = :post
177
- request[:type] = :finish
178
- else
179
- #
180
- # Send the part upload request
181
- #
182
- options[:object_options][:headers]['Content-Md5'] = options[:file_id] if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil?
183
- options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil?
184
- options[:object_options][:parameters]['partNumber'] = options[:part]
185
- options[:object_options][:verb] = :put
186
- request[:type] = :part_upload
187
- end
188
-
189
-
190
- #
191
- # Set the upload
192
- #
193
- options[:object_options][:parameters]['uploadId'] = options[:resumable_id]
194
-
195
-
196
- #
197
- # provide the signed request
198
- #
199
- request[:signature] = sign_request(options)
200
- request
201
- end
202
-
203
-
204
- def fog_connection
205
- @fog = @fog || Fog::Storage.new(@options[:fog])
206
- return @fog
207
- end
208
-
209
-
210
- def destroy(upload)
211
- connection = fog_connection
212
- directory = connection.directories.get(upload.bucket_name) # it is assumed this exists - if not then the upload wouldn't have taken place
213
- file = directory.files.get(upload.object_key)
214
-
215
- if upload.resumable
216
- return file.destroy unless file.nil?
217
- begin
218
- if upload.resumable_id.present?
219
- connection.abort_multipart_upload(upload.bucket_name, upload.object_key, upload.resumable_id)
220
- return true
221
- end
222
- rescue
223
- # In-case resumable_id was invalid or did not match the object key
224
- end
225
-
226
- #
227
- # The user may have provided an invalid upload key, we'll need to search for the upload and destroy it
228
- #
229
- begin
230
- resp = connection.list_multipart_uploads(upload.bucket_name, {'prefix' => upload.object_key})
231
- resp.body['Upload'].each do |file|
232
- #
233
- # TODO:: BUGBUG:: there is an edge case where there may be more multi-part uploads with this this prefix then will be provided in a single request
234
- # => We'll need to handle this edge case to avoid abuse and dangling objects
235
- #
236
- connection.abort_multipart_upload(upload.bucket_name, upload.object_key, file['UploadId']) if file['Key'] == upload.object_key # Ensure an exact match
237
- end
238
- return true # The upload was either never initialised or has been destroyed
239
- rescue
240
- return false
241
- end
242
- else
243
- return true if file.nil?
244
- return file.destroy
245
- end
246
- end
247
-
248
-
249
-
250
- protected
251
-
252
-
253
-
254
- def sign_request(options)
255
-
256
- #
257
- # Build base URL
258
- #
259
- options[:object_options][:date] = options[:object_options][:date].utc.httpdate
260
- options[:object_options][:expires] = options[:object_options][:expires].utc.to_i
261
- url = "/#{options[:bucket_name]}/#{options[:object_key]}"
262
-
263
- #
264
- # Add request params
265
- #
266
- url << '?'
267
- options[:object_options][:parameters].each do |key, value|
268
- url += value.empty? ? "#{key}&" : "#{key}=#{value}&"
269
- end
270
- url.chop!
271
-
272
- #
273
- # Build a request signature
274
- #
275
- signature = "#{options[:object_options][:verb].to_s.upcase}\n#{options[:file_id]}\n#{options[:object_options][:headers]['Content-Type']}\n#{options[:object_options][:expires]}\n"
276
- options[:object_options][:headers].each do |key, value|
277
- signature << "#{key}:#{value}\n" if key =~ /x-amz-/
278
- end
279
- signature << url
280
-
281
-
282
- #
283
- # Encode the request signature
284
- #
285
- signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), @options[:secret_key], signature)).gsub("\n",""))
286
-
287
-
288
- #
289
- # Finish building the request
290
- #
291
- url += options[:object_options][:parameters].present? ? '&' : '?'
292
- return {
293
- :verb => options[:object_options][:verb].to_s.upcase,
294
- :url => "#{options[:object_options][:protocol]}://#{options[:region]}#{url}AWSAccessKeyId=#{@options[:access_id]}&Expires=#{options[:object_options][:expires]}&Signature=#{signature}",
295
- :headers => options[:object_options][:headers]
296
- }
297
- end
298
-
299
-
6
+
7
+ def initialize(options)
8
+ @options = {
9
+ :name => :AmazonS3,
10
+ :location => :'us-east-1',
11
+ :fog => {
12
+ :provider => :AWS,
13
+ :aws_access_key_id => options[:access_id],
14
+ :aws_secret_access_key => options[:secret_key],
15
+ :region => (options[:location] || 'us-east-1')
16
+ }
17
+ }.merge!(options)
18
+
19
+
20
+ raise ArgumentError, 'Amazon Access ID missing' if @options[:access_id].nil?
21
+ raise ArgumentError, 'Amazon Secret Key missing' if @options[:secret_key].nil?
22
+
23
+
24
+ @options[:location] = @options[:location].to_sym
25
+ @options[:region] = @options[:location] == :'us-east-1' ? 's3.amazonaws.com' : "s3-#{@options[:location]}.amazonaws.com"
26
+ end
27
+
28
+
29
+ def name
30
+ @options[:name]
31
+ end
32
+
33
+
34
+ def location
35
+ @options[:location]
36
+ end
37
+
38
+
39
+ # Create a signed URL for accessing a private file
40
+ def get_object(options)
41
+ options = {}.merge!(options) # Need to deep copy here
42
+ options[:object_options] = {
43
+ :expires => 5.minutes.from_now,
44
+ :date => Time.now,
45
+ :verb => :get, # Post for multi-part uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html
46
+ :headers => {},
47
+ :parameters => {},
48
+ :protocol => :https
49
+ }.merge!(options[:object_options] || {})
50
+ options.merge!(@options)
51
+
52
+ #
53
+ # provide the signed request
54
+ #
55
+ sign_request(options)[:url]
56
+ end
57
+
58
+
59
+ # Creates a new upload request (either single shot or multi-part)
60
+ # => Passed: bucket_name, object_key, object_options, file_size
61
+ def new_upload(options)
62
+ options = {}.merge!(options) # Need to deep copy here
63
+ options[:object_options] = {
64
+ :permissions => :private,
65
+ :expires => 5.minutes.from_now,
66
+ :date => Time.now,
67
+ :verb => :post, # Post for multi-part uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html
68
+ :headers => {},
69
+ :parameters => {},
70
+ :protocol => :https
71
+ }.merge!(options[:object_options])
72
+ options.merge!(@options)
73
+
74
+ # Set the access control headers
75
+ if options[:object_options][:headers]['x-amz-acl'].nil?
76
+ options[:object_options][:headers]['x-amz-acl'] = case options[:object_options][:permissions]
77
+ when :public
78
+ :'public-read'
79
+ else
80
+ :private
81
+ end
82
+ end
83
+
84
+ # Decide what type of request is being sent
85
+ request = {}
86
+ if options[:file_size] > 5.megabytes # 5 mb (minimum chunk size)
87
+ options[:object_options][:parameters][:uploads] = '' # Customise the request to be a chunked upload
88
+ options.delete(:file_id) # Does not apply to chunked uploads
89
+
90
+ request[:type] = :chunked_upload
91
+ else
92
+ if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil?
93
+ # The client side is sending hex formatted ids that will match the amazon etag
94
+ # => We need this to be base64 for the md5 header (this is now done at the client side)
95
+ #
96
+ # options[:file_id] = [[options[:file_id]].pack("H*")].pack("m0") # (the 0 avoids the call to strip - now done client side)
97
+ # [ options[:file_id] ].pack('m').strip # This wasn't correct
98
+ # Base64.encode64(options[:file_id]).strip # This also wasn't correct
99
+ options[:object_options][:headers]['Content-Md5'] = options[:file_id]
100
+ end
101
+ options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil?
102
+ options[:object_options][:verb] = :put # Put for direct uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectPUT.html
103
+
104
+ request[:type] = :direct_upload
105
+ end
106
+
107
+
108
+ # provide the signed request
109
+ request[:signature] = sign_request(options)
110
+ request
111
+ end
112
+
113
+
114
+ # Returns the request to get the parts of a resumable upload
115
+ def get_parts(options)
116
+ options[:object_options] = {
117
+ :expires => 5.minutes.from_now,
118
+ :date => Time.now,
119
+ :verb => :get,
120
+ :headers => {},
121
+ :parameters => {},
122
+ :protocol => :https
123
+ }.merge!(options[:object_options])
124
+ options.merge!(@options)
125
+
126
+ # Set the upload
127
+ options[:object_options][:parameters]['uploadId'] = options[:resumable_id]
128
+
129
+ # provide the signed request
130
+ {
131
+ :type => :parts,
132
+ :signature => sign_request(options)
133
+ }
134
+ end
135
+
136
+
137
+ # Returns the requests for uploading parts and completing a resumable upload
138
+ def set_part(options)
139
+ options[:object_options] = {
140
+ :expires => 5.minutes.from_now,
141
+ :date => Time.now,
142
+ :headers => {},
143
+ :parameters => {},
144
+ :protocol => :https
145
+ }.merge!(options[:object_options])
146
+ options.merge!(@options)
147
+
148
+
149
+ request = {}
150
+ if options[:part] == 'finish'
151
+ # Send the commitment response
152
+ options[:object_options][:headers]['Content-Type'] = 'application/xml; charset=UTF-8' if options[:object_options][:headers]['Content-Type'].nil?
153
+ options[:object_options][:verb] = :post
154
+ request[:type] = :finish
155
+ else
156
+ # Send the part upload request
157
+ options[:object_options][:headers]['Content-Md5'] = options[:file_id] if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil?
158
+ options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil?
159
+ options[:object_options][:parameters]['partNumber'] = options[:part]
160
+ options[:object_options][:verb] = :put
161
+ request[:type] = :part_upload
162
+ end
163
+
164
+
165
+ # Set the upload
166
+ options[:object_options][:parameters]['uploadId'] = options[:resumable_id]
167
+
168
+ # provide the signed request
169
+ request[:signature] = sign_request(options)
170
+ request
171
+ end
172
+
173
+
174
+ def fog_connection
175
+ @fog = @fog || Fog::Storage.new(@options[:fog])
176
+ return @fog
177
+ end
178
+
179
+
180
+ def destroy(upload)
181
+ connection = fog_connection
182
+ directory = connection.directories.get(upload.bucket_name) # it is assumed this exists - if not then the upload wouldn't have taken place
183
+ file = directory.files.get(upload.object_key)
184
+
185
+ if upload.resumable
186
+ return file.destroy unless file.nil?
187
+ begin
188
+ if upload.resumable_id.present?
189
+ connection.abort_multipart_upload(upload.bucket_name, upload.object_key, upload.resumable_id)
190
+ return true
191
+ end
192
+ rescue
193
+ # In-case resumable_id was invalid or did not match the object key
194
+ end
195
+
196
+ # The user may have provided an invalid upload key, we'll need to search for the upload and destroy it
197
+ begin
198
+ resp = connection.list_multipart_uploads(upload.bucket_name, {'prefix' => upload.object_key})
199
+ resp.body['Upload'].each do |file|
200
+ # TODO:: BUGBUG:: there is an edge case where there may be more multi-part uploads with this this prefix then will be provided in a single request
201
+ # => We'll need to handle this edge case to avoid abuse and dangling objects
202
+ connection.abort_multipart_upload(upload.bucket_name, upload.object_key, file['UploadId']) if file['Key'] == upload.object_key # Ensure an exact match
203
+ end
204
+ return true # The upload was either never initialised or has been destroyed
205
+ rescue
206
+ return false
207
+ end
208
+ else
209
+ return true if file.nil?
210
+ return file.destroy
211
+ end
212
+ end
213
+
214
+
215
+
216
+ protected
217
+
218
+
219
+
220
+ def sign_request(options)
221
+
222
+ # Build base URL
223
+ options[:object_options][:date] = options[:object_options][:date].utc.httpdate
224
+ options[:object_options][:expires] = options[:object_options][:expires].utc.to_i
225
+ url = "/#{options[:bucket_name]}/#{options[:object_key]}"
226
+
227
+ # Add request params
228
+ url << '?'
229
+ options[:object_options][:parameters].each do |key, value|
230
+ url += value.blank? ? "#{key}&" : "#{key}=#{value}&"
231
+ end
232
+ url.chop!
233
+
234
+ # Build a request signature
235
+ signature = "#{options[:object_options][:verb].to_s.upcase}\n#{options[:file_id]}\n#{options[:object_options][:headers]['Content-Type']}\n#{options[:object_options][:expires]}\n"
236
+ options[:object_options][:headers].each do |key, value|
237
+ signature << "#{key}:#{value}\n" if key =~ /x-amz-/
238
+ end
239
+ signature << url
240
+
241
+ # Encode the request signature
242
+ signature = CGI::escape(Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), @options[:secret_key], signature)).gsub("\n",""))
243
+
244
+ # Finish building the request
245
+ url += options[:object_options][:parameters].present? ? '&' : '?'
246
+ return {
247
+ :verb => options[:object_options][:verb].to_s.upcase,
248
+ :url => "#{options[:object_options][:protocol]}://#{options[:region]}#{url}AWSAccessKeyId=#{@options[:access_id]}&Expires=#{options[:object_options][:expires]}&Signature=#{signature}",
249
+ :headers => options[:object_options][:headers]
250
+ }
251
+ end
300
252
  end
301
253