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,306 @@
1
+ module Condo; end
2
+ module Condo::Strata; end
3
+
4
+
5
+ class Fog::Storage::Google::Real
6
+ def condo_request(*args)
7
+ request(*args)
8
+ end
9
+ end
10
+
11
+
12
+ class Condo::Strata::GoogleCloudStorage
13
+
14
+ def initialize(options)
15
+ @options = {
16
+ :name => :GoogleCloudStorage,
17
+ :location => :na, # US or Europe, set at bucket creation time
18
+ :fog => {
19
+ :provider => 'Google',
20
+ :google_storage_access_key_id => options[:access_id],
21
+ :google_storage_secret_access_key => options[:secret_key]
22
+ }
23
+ }.merge!(options)
24
+
25
+
26
+ raise ArgumentError, 'Google Access ID missing' if @options[:access_id].nil?
27
+ raise ArgumentError, 'Google Secret Key missing' if @options[:secret_key].nil?
28
+
29
+
30
+ @options[:location] = @options[:location].to_sym
31
+ end
32
+
33
+
34
+ #
35
+ # Enable CORS on a bucket for a domain
36
+ #
37
+ def enable_cors(bucket, origin = '*')
38
+ data =
39
+ <<-DATA
40
+ <?xml version="1.0" encoding="UTF-8"?>
41
+ <CorsConfig>
42
+ <Cors>
43
+ <Origins>
44
+ <Origin>#{origin}</Origin>
45
+ </Origins>
46
+ <Methods>
47
+ <Method>GET</Method>
48
+ <Method>HEAD</Method>
49
+ <Method>POST</Method>
50
+ <Method>PUT</Method>
51
+ </Methods>
52
+ <ResponseHeaders>
53
+ <ResponseHeader>origin</ResponseHeader>
54
+ <ResponseHeader>content-md5</ResponseHeader>
55
+ <ResponseHeader>authorization</ResponseHeader>
56
+ <ResponseHeader>x-goog-date</ResponseHeader>
57
+ <ResponseHeader>x-goog-acl</ResponseHeader>
58
+ <ResponseHeader>content-type</ResponseHeader>
59
+ <ResponseHeader>accept</ResponseHeader>
60
+ <ResponseHeader>x-goog-api-version</ResponseHeader>
61
+ <ResponseHeader>x-goog-resumable</ResponseHeader>
62
+ </ResponseHeaders>
63
+ <MaxAgeSec>1800</MaxAgeSec>
64
+ </Cors>
65
+ </CorsConfig>
66
+ DATA
67
+
68
+ fog_connection.condo_request(
69
+ :expects => 200,
70
+ :body => data,
71
+ :method => 'PUT',
72
+ :headers => {},
73
+ :host => "#{bucket}.storage.googleapis.com",
74
+ :idempotent => true,
75
+ :path => '?cors' # There is an issue with Fog where this isn't included as a canonical_resource
76
+ )
77
+ end
78
+
79
+
80
+ def name
81
+ @options[:name]
82
+ end
83
+
84
+
85
+ def location
86
+ @options[:location]
87
+ end
88
+
89
+
90
+ #
91
+ # Create a signed URL for accessing a private file
92
+ #
93
+ def get_object(options)
94
+ options = {}.merge!(options) # Need to deep copy here
95
+ options[:object_options] = {
96
+ :expires => 5.minutes.from_now,
97
+ :verb => :get,
98
+ :headers => {},
99
+ :parameters => {},
100
+ :protocol => :https
101
+ }.merge!(options[:object_options] || {})
102
+ options.merge!(@options)
103
+
104
+ #
105
+ # provide the signed request
106
+ #
107
+ sign_request(options)[:url]
108
+ end
109
+
110
+
111
+ #
112
+ # Creates a new upload request (either single shot or multi-part)
113
+ # => Passed: bucket_name, object_key, object_options, file_size
114
+ #
115
+ def new_upload(options)
116
+ options = {}.merge!(options) # Need to deep copy here
117
+ options[:object_options] = {
118
+ :permissions => :private,
119
+ :expires => 5.minutes.from_now,
120
+ :verb => :put, # This will be a post for resumable uploads
121
+ :headers => {},
122
+ :parameters => {},
123
+ :protocol => :https
124
+ }.merge!(options[:object_options] || {})
125
+ options.merge!(@options)
126
+
127
+
128
+ #
129
+ # Set the access control headers
130
+ #
131
+ options[:object_options][:headers]['x-goog-api-version'] = 1
132
+
133
+ if options[:object_options][:headers]['x-goog-acl'].nil?
134
+ options[:object_options][:headers]['x-goog-acl'] = case options[:object_options][:permissions]
135
+ when :public
136
+ :'public-read'
137
+ else
138
+ :private
139
+ end
140
+ end
141
+
142
+ options[:object_options][:headers]['Content-Md5'] = options[:file_id] if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil?
143
+ options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil?
144
+
145
+
146
+ #
147
+ # Decide what type of request is being sent
148
+ # => Currently google only supports direct uploads (no CORS resumables yet!)
149
+ #
150
+ return {
151
+ :signature => sign_request(options),
152
+ :type => :direct_upload
153
+ }
154
+
155
+ #
156
+ # This is what we'll return when resumables work with CORS
157
+ #
158
+ if options[:file_size] > 2.megabytes
159
+ # Resumables may not support the md5 header at this time - have to compare ETag and fail on the client side
160
+ options[:object_options][:verb] = :post
161
+ options[:object_options][:headers]['x-goog-resumable'] = 'start'
162
+ return {
163
+ :signature => sign_request(options),
164
+ :type => :chunked_upload # triggers resumable
165
+ }
166
+ end
167
+ end
168
+
169
+
170
+ #
171
+ # Creates a request for the byte we were up to
172
+ # => doesn't work with CORS yet
173
+ #
174
+ def get_parts(options)
175
+ options[:object_options] = {
176
+ :expires => 5.minutes.from_now,
177
+ :verb => :put,
178
+ :headers => {},
179
+ :parameters => {},
180
+ :protocol => :https
181
+ }.merge!(options[:object_options])
182
+ options.merge!(@options)
183
+
184
+ #
185
+ # Set the upload and request the range of bytes we are after
186
+ #
187
+ options[:object_options][:parameters]['upload_id'] = options[:resumable_id]
188
+ ## This can be set on the client side as it is not part of the signed request
189
+ #options[:object_options][:headers]['Content-Range'] = "bytes */#{options[:file_size]}"
190
+
191
+ #
192
+ # provide the signed request
193
+ #
194
+ {
195
+ :type => :parts,
196
+ :signature => sign_request(options)
197
+ }
198
+ end
199
+
200
+
201
+ #
202
+ # Returns the requests for uploading parts and completing a resumable upload
203
+ #
204
+ def set_part(options)
205
+ resp = get_parts(options)
206
+ resp[:type] = :part_upload
207
+ return resp
208
+ end
209
+
210
+
211
+ def fog_connection
212
+ @fog = @fog || Fog::Storage.new(@options[:fog])
213
+ return @fog
214
+ end
215
+
216
+
217
+ def destroy(upload)
218
+ connection = fog_connection
219
+ directory = connection.directories.get(upload.bucket_name) # it is assumed this exists - if not then the upload wouldn't have taken place
220
+ file = directory.files.get(upload.object_key) # NOTE:: I only assume this works with resumables... should look into it
221
+
222
+ return true if file.nil?
223
+ return file.destroy
224
+ end
225
+
226
+
227
+
228
+ protected
229
+
230
+
231
+
232
+ def sign_request(options)
233
+
234
+ #
235
+ # Build base URL
236
+ #
237
+ verb = options[:object_options][:verb].to_s.upcase.to_sym
238
+ options[:object_options][:expires] = options[:object_options][:expires].utc.to_i
239
+
240
+ url = "/#{options[:object_key]}"
241
+
242
+
243
+ #
244
+ # Add signed request params
245
+ #
246
+ other_params = ''
247
+ signed_params = '?'
248
+ (options[:object_options][:parameters] || {}).each do |key, value|
249
+ if ['acl', 'cors', 'location', 'logging', 'requestPayment', 'torrent', 'versions', 'versioning'].include?(key)
250
+ signed_params << "#{key}&"
251
+ else
252
+ other_params << (value.empty? ? "#{key}&" : "#{key}=#{value}&")
253
+ end
254
+ end
255
+ signed_params.chop!
256
+
257
+ url << signed_params
258
+
259
+
260
+ #
261
+ # Build a request signature
262
+ #
263
+ signature = "#{verb}\n#{options[:file_id]}\n#{options[:object_options][:headers]['Content-Type']}\n#{options[:object_options][:expires]}\n"
264
+ if verb != :GET
265
+ options[:object_options][:headers]['x-goog-date'] ||= Time.now.utc.httpdate
266
+
267
+ google_headers, canonical_google_headers = {}, '' # Copied from https://github.com/fog/fog/blob/master/lib/fog/google/storage.rb
268
+ for key, value in options[:object_options][:headers]
269
+ if key[0..6] == 'x-goog-'
270
+ google_headers[key] = value
271
+ end
272
+ end
273
+
274
+ google_headers = google_headers.sort {|x, y| x[0] <=> y[0]}
275
+ for key, value in google_headers
276
+ signature << "#{key}:#{value}\n"
277
+ end
278
+ end
279
+
280
+ signature << "/#{options[:bucket_name]}#{url}"
281
+
282
+
283
+ #
284
+ # Encode the request signature
285
+ #
286
+ signature = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), @options[:secret_key], signature)).chomp!
287
+
288
+
289
+ url += signed_params.present? ? '&' : '?'
290
+ url = "#{options[:object_options][:protocol]}://#{options[:bucket_name]}.storage.googleapis.com#{url}#{other_params}GoogleAccessId=#{@options[:access_id]}&Expires=#{options[:object_options][:expires]}&Signature=#{CGI::escape(signature)}"
291
+ options[:object_options][:headers]['Authorization'] = "GOOG1 #{@options[:access_id]}:#{signature}"
292
+
293
+
294
+ #
295
+ # Finish building the request
296
+ #
297
+ return {
298
+ :verb => options[:object_options][:verb].to_s.upcase,
299
+ :url => url,
300
+ :headers => options[:object_options][:headers]
301
+ }
302
+ end
303
+
304
+
305
+ end
306
+
@@ -0,0 +1,223 @@
1
+ module Condo; end
2
+ module Condo::Strata; end
3
+
4
+ #
5
+ # NOTE:: Set Account Metadata Key for Public Access before this will work - X-Account-Meta-Temp-Url-Key: <your key>
6
+ #
7
+
8
+ class Condo::Strata::RackspaceCloudFiles
9
+
10
+ def initialize(options)
11
+ @options = {
12
+ :name => :RackspaceCloudFiles,
13
+ :location => :na, # dallas or chicago - this is set at bucket creation time
14
+ :fog => {
15
+ :provider => 'Rackspace',
16
+ :rackspace_username => options[:username],
17
+ :rackspace_api_key => options[:secret_key],
18
+ :rackspace_auth_url => options[:auth_url] || 'identity.api.rackspacecloud.com' # is US and UK is 'lon.auth.api.rackspacecloud.com'
19
+ }
20
+ }.merge!(options)
21
+
22
+
23
+ raise ArgumentError, 'Rackspace Username missing' if @options[:username].nil?
24
+ raise ArgumentError, 'Rackspace Secret Key missing' if @options[:secret_key].nil?
25
+
26
+
27
+ @options[:location] = @options[:location].to_sym
28
+ end
29
+
30
+
31
+ def name
32
+ @options[:name]
33
+ end
34
+
35
+
36
+ def location
37
+ @options[:location]
38
+ end
39
+
40
+
41
+ #
42
+ # Here for convenience
43
+ #
44
+ def set_metatdata_key(key)
45
+ fog_connection.request(
46
+ :expects => [201, 202, 204],
47
+ :method => 'POST',
48
+ :headers => {'X-Account-Meta-Temp-Url-Key' => key}
49
+ )
50
+ end
51
+
52
+
53
+ #
54
+ # Create a signed URL for accessing a private file
55
+ #
56
+ def get_object(options)
57
+ options = {}.merge!(options) # Need to deep copy here
58
+ options[:object_options] = {
59
+ :expires => 5.minutes.from_now,
60
+ :verb => :get, # Post for multi-part uploads http://docs.amazonwebservices.com/AmazonS3/latest/API/mpUploadInitiate.html
61
+ :headers => {},
62
+ :parameters => {}
63
+ }.merge!(options[:object_options] || {})
64
+ options.merge!(@options)
65
+
66
+ #
67
+ # provide the signed request
68
+ #
69
+ sign_request(options)[:url]
70
+ end
71
+
72
+
73
+ #
74
+ # Creates a new upload request (either single shot or multi-part)
75
+ # => Passed: bucket_name, object_key, object_options, file_size
76
+ #
77
+ def new_upload(options)
78
+ options = {}.merge!(options) # Need to deep copy here
79
+ options[:object_options] = {
80
+ :expires => 5.minutes.from_now,
81
+ :verb => :put,
82
+ :headers => {},
83
+ :parameters => {}
84
+ }.merge!(options[:object_options])
85
+ options.merge!(@options)
86
+
87
+ options[:object_options][:headers]['ETag'] = options[:file_id] if options[:file_id].present? && options[:object_options][:headers]['ETag'].nil?
88
+ options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil?
89
+
90
+
91
+ #
92
+ # Decide what type of request is being sent
93
+ #
94
+ request = {}
95
+ if options[:file_size] > 2097152 # 2 mb (minimum chunk size)
96
+
97
+ options[:object_key] = options[:object_key] + '_p1' # Append the part number
98
+ request[:type] = :chunked_upload
99
+ else
100
+
101
+ request[:type] = :direct_upload
102
+ end
103
+
104
+ #
105
+ # provide the signed request
106
+ #
107
+ request[:signature] = sign_request(options)
108
+ request
109
+ end
110
+
111
+
112
+ #
113
+ # Returns the part we are up to
114
+ #
115
+ def get_parts(options)
116
+ {
117
+ :type => :parts,
118
+ :current_part => options[:resumable_id]
119
+ }
120
+ end
121
+
122
+
123
+ #
124
+ # Returns the requests for uploading parts and completing a resumable upload
125
+ #
126
+ def set_part(options)
127
+ options[:object_options] = {
128
+ :expires => 5.minutes.from_now,
129
+ :headers => {},
130
+ :parameters => {},
131
+ :verb => :put
132
+ }.merge!(options[:object_options])
133
+ options.merge!(@options)
134
+
135
+
136
+ request = {}
137
+ if options[:part] == 'finish'
138
+ #
139
+ # Send the commitment response
140
+ #
141
+ options[:object_options][:headers]['X-Object-Manifest'] = "#{options[:bucket_name]}/#{options[:object_key]}"
142
+ request[:type] = :finish
143
+ else
144
+ #
145
+ # Send the part upload request
146
+ #
147
+ options[:object_options][:headers]['Content-Md5'] = options[:file_id] if options[:file_id].present? && options[:object_options][:headers]['Content-Md5'].nil?
148
+ options[:object_options][:headers]['Content-Type'] = 'binary/octet-stream' if options[:object_options][:headers]['Content-Type'].nil?
149
+ options[:object_key] = options[:object_key] + '_p' + options[:part]
150
+ request[:type] = :part_upload
151
+ end
152
+
153
+ #
154
+ # provide the signed request
155
+ #
156
+ request[:signature] = sign_request(options)
157
+ request
158
+ end
159
+
160
+
161
+ def fog_connection
162
+ @fog = @fog || Fog::Storage.new(@options[:fog])
163
+ return @fog
164
+ end
165
+
166
+
167
+ def destroy(upload)
168
+ connection = fog_connection
169
+ directory = connection.directories.get(upload.bucket_name) # it is assumed this exists - if not then the upload wouldn't have taken place
170
+
171
+ if upload.resumable
172
+ directory.files.all({'prefix' => upload.object_key}).each do |file|
173
+ return false unless file.destroy
174
+ end
175
+ end
176
+
177
+ file = directory.files.get(upload.object_key) # this is the manifest when resumable
178
+
179
+ return true if file.nil?
180
+ return file.destroy
181
+ end
182
+
183
+
184
+
185
+ protected
186
+
187
+
188
+
189
+ def sign_request(options)
190
+
191
+ #
192
+ # Build base URL
193
+ #
194
+ options[:object_options][:expires] = options[:object_options][:expires].utc.to_i
195
+ url = "/v1/#{options[:username]}/#{CGI::escape options[:bucket_name]}/#{CGI::escape options[:object_key]}"
196
+
197
+
198
+
199
+ #
200
+ # Build a request signature
201
+ #
202
+ signature = "#{options[:object_options][:verb].to_s.upcase}\n#{options[:object_options][:expires]}\n#{url}"
203
+
204
+
205
+ #
206
+ # Encode the request signature
207
+ #
208
+ signature = OpenSSL::HMAC.hexdigest('sha1', @options[:secret_key], signature)
209
+
210
+
211
+ #
212
+ # Finish building the request
213
+ #
214
+ return {
215
+ :verb => options[:object_options][:verb].to_s.upcase,
216
+ :url => "https://storage.clouddrive.com#{url}?temp_url_sig=#{signature}&temp_url_expires=#{options[:object_options][:expires]}",
217
+ :headers => options[:object_options][:headers]
218
+ }
219
+ end
220
+
221
+
222
+ end
223
+