condo 0.0.1

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 (54) hide show
  1. data/LGPL3-LICENSE +165 -0
  2. data/README.textile +20 -0
  3. data/Rakefile +40 -0
  4. data/app/assets/javascripts/condo.js +7 -0
  5. data/app/assets/javascripts/condo/amazon.js +409 -0
  6. data/app/assets/javascripts/condo/base64.js +192 -0
  7. data/app/assets/javascripts/condo/controller.js +162 -0
  8. data/app/assets/javascripts/condo/google.js +292 -0
  9. data/app/assets/javascripts/condo/rackspace.js +340 -0
  10. data/app/assets/javascripts/condo/spark-md5.js +470 -0
  11. data/app/assets/javascripts/condo/uploader.js +298 -0
  12. data/lib/condo.rb +267 -0
  13. data/lib/condo/configuration.rb +129 -0
  14. data/lib/condo/engine.rb +36 -0
  15. data/lib/condo/errors.rb +9 -0
  16. data/lib/condo/strata/amazon_s3.rb +301 -0
  17. data/lib/condo/strata/google_cloud_storage.rb +306 -0
  18. data/lib/condo/strata/rackspace_cloud_files.rb +223 -0
  19. data/lib/condo/version.rb +3 -0
  20. data/lib/tasks/condo_tasks.rake +4 -0
  21. data/test/condo_test.rb +27 -0
  22. data/test/dummy/README.rdoc +261 -0
  23. data/test/dummy/Rakefile +7 -0
  24. data/test/dummy/app/assets/javascripts/application.js +15 -0
  25. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  26. data/test/dummy/app/controllers/application_controller.rb +3 -0
  27. data/test/dummy/app/helpers/application_helper.rb +2 -0
  28. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  29. data/test/dummy/config.ru +4 -0
  30. data/test/dummy/config/application.rb +59 -0
  31. data/test/dummy/config/boot.rb +10 -0
  32. data/test/dummy/config/database.yml +25 -0
  33. data/test/dummy/config/environment.rb +5 -0
  34. data/test/dummy/config/environments/development.rb +37 -0
  35. data/test/dummy/config/environments/production.rb +67 -0
  36. data/test/dummy/config/environments/test.rb +37 -0
  37. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  38. data/test/dummy/config/initializers/inflections.rb +15 -0
  39. data/test/dummy/config/initializers/mime_types.rb +5 -0
  40. data/test/dummy/config/initializers/secret_token.rb +7 -0
  41. data/test/dummy/config/initializers/session_store.rb +8 -0
  42. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  43. data/test/dummy/config/locales/en.yml +5 -0
  44. data/test/dummy/config/routes.rb +58 -0
  45. data/test/dummy/db/test.sqlite3 +0 -0
  46. data/test/dummy/log/test.log +25 -0
  47. data/test/dummy/public/404.html +26 -0
  48. data/test/dummy/public/422.html +26 -0
  49. data/test/dummy/public/500.html +25 -0
  50. data/test/dummy/public/favicon.ico +0 -0
  51. data/test/dummy/script/rails +6 -0
  52. data/test/integration/navigation_test.rb +10 -0
  53. data/test/test_helper.rb +15 -0
  54. metadata +180 -0
@@ -0,0 +1,129 @@
1
+ require 'singleton'
2
+
3
+ module Condo
4
+
5
+ class Configuration
6
+ include Singleton
7
+
8
+ @@callbacks = {
9
+ #:resident_id # Must be defined by the including class
10
+ :bucket_name => proc {"#{Rails.application.class.parent_name}#{instance_eval @@callbacks[:resident_id]}"},
11
+ :object_key => proc {params[:file_name]},
12
+ :object_options => proc {{:permissions => :private}},
13
+ :pre_validation => proc {true}, # To respond with errors use: lambda {return false, {:errors => {:param_name => 'wtf are you doing?'}}}
14
+ :sanitize_filename => proc {
15
+ params[:file_name].tap do |filename|
16
+ filename.gsub!(/^.*(\\|\/)/, '') # get only the filename (just in case)
17
+ filename.gsub!(/[^\w\.\-]/,'_') # replace all non alphanumeric or periods with underscore
18
+ end
19
+ }
20
+ #:upload_complete # Must be defined by the including class
21
+ #:destroy_upload # the actual delete should be done by the application
22
+ #:dynamic_residence # If the data stores are dynamically stored by the application
23
+ }
24
+
25
+ @@dynamics = {}
26
+
27
+ def self.callbacks
28
+ @@callbacks
29
+ end
30
+
31
+ def self.set_callback(name, callback = nil, &block)
32
+ if callback.is_a?(Proc)
33
+ @@callbacks[name.to_sym] = callback
34
+ elsif block.present?
35
+ @@callbacks[name.to_sym] = block
36
+ else
37
+ raise ArgumentError, 'Condo callbacks must be defined with a Proc or Proc (lamba) object present'
38
+ end
39
+ end
40
+
41
+
42
+ #
43
+ # Provides a callback whenever attempting to select a provider for the current request
44
+ # => Allows multiple providers for different users / controllers or dynamic providers
45
+ #
46
+ def self.set_dynamic_provider(namespace, callback = nil, &block)
47
+ if callback.is_a?(Proc)
48
+ @@dynamics[namespace.to_sym] = callback
49
+ elsif block.present?
50
+ @@dynamics[namespace.to_sym] = block
51
+ else
52
+ raise ArgumentError, 'Condo callbacks must be defined with a Proc or Proc (lamba) object present'
53
+ end
54
+ end
55
+
56
+ def dynamic_provider_present?(namespace)
57
+ return false if @@dynamics.nil? || @@dynamics[namespace.to_sym].nil?
58
+ true
59
+ end
60
+
61
+
62
+ #
63
+ # Allows for predefined storage providers (maybe you only use Amazon?)
64
+ #
65
+ def self.add_residence(name, options = {})
66
+ @@residencies ||= []
67
+ @@residencies << ("Condo::Strata::#{name.to_s.camelize}".constantize.new(options)).tap do |res|
68
+ name = name.to_sym
69
+ namespace = (options[:namespace] || :global).to_sym
70
+
71
+ @@locations ||= {}
72
+ @@locations[namespace] ||= {}
73
+ @@locations[namespace][name] ||= {}
74
+
75
+ if options[:location].present?
76
+ @@locations[namespace][name][options[:location].to_sym] = res
77
+ else
78
+ @@locations[namespace][name][:default] = res
79
+ @@locations[namespace][name][res.location] = res
80
+ end
81
+ end
82
+ end
83
+
84
+
85
+ def residencies
86
+ @@residencies
87
+ end
88
+
89
+
90
+ #
91
+ # Storage provider selection routine
92
+ # => pass in :dynamic => true with :name and connection options to create a new instance
93
+ #
94
+ def set_residence(name, options)
95
+ if options[:namespace].present? && dynamic_provider_present?(options[:namespace])
96
+ if options[:upload].present?
97
+ upload = options[:upload]
98
+ params = {
99
+ :user_id => upload.user_id,
100
+ :file_name => upload.file_name,
101
+ :file_size => upload.file_size,
102
+ :custom_params => upload.custom_params,
103
+ :provider_name => upload.provider_name,
104
+ :provider_location => upload.provider_location,
105
+ :provider_namespace => upload.provider_namespace
106
+ }
107
+ return instance_exec params, &@@dynamics[upload.provider_namespace]
108
+ else
109
+ params = {
110
+ :user_id => options[:resident],
111
+ :file_name => options[:params][:file_name],
112
+ :file_size => options[:params][:file_size],
113
+ :custom_params => options[:params][:custom_params],
114
+ :provider_namespace => options[:namespace]
115
+ }
116
+ return instance_exec params, &@@dynamics[options[:namespace]]
117
+ end
118
+ else
119
+ if !!options[:dynamic]
120
+ return "Condo::Strata::#{name.to_s.camelize}".constantize.new(options)
121
+ else
122
+ return options[:location].present? ? @@locations[options[:namespace]][name.to_sym][options[:location].to_sym] : @@locations[options[:namespace]][name.to_sym][:default]
123
+ end
124
+ end
125
+ end
126
+
127
+ end
128
+
129
+ end
@@ -0,0 +1,36 @@
1
+ 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
+ # Set the proper error types for Rails.
18
+ #
19
+ initializer "load http errors" 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
+
34
+
35
+ end
36
+ end
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,301 @@
1
+ module Condo; end
2
+ module Condo::Strata; end
3
+
4
+
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
+
300
+ end
301
+