condo 0.0.1

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