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.
- data/LGPL3-LICENSE +165 -0
- data/README.textile +20 -0
- data/Rakefile +40 -0
- data/app/assets/javascripts/condo.js +7 -0
- data/app/assets/javascripts/condo/amazon.js +409 -0
- data/app/assets/javascripts/condo/base64.js +192 -0
- data/app/assets/javascripts/condo/controller.js +162 -0
- data/app/assets/javascripts/condo/google.js +292 -0
- data/app/assets/javascripts/condo/rackspace.js +340 -0
- data/app/assets/javascripts/condo/spark-md5.js +470 -0
- data/app/assets/javascripts/condo/uploader.js +298 -0
- data/lib/condo.rb +267 -0
- data/lib/condo/configuration.rb +129 -0
- data/lib/condo/engine.rb +36 -0
- data/lib/condo/errors.rb +9 -0
- data/lib/condo/strata/amazon_s3.rb +301 -0
- data/lib/condo/strata/google_cloud_storage.rb +306 -0
- data/lib/condo/strata/rackspace_cloud_files.rb +223 -0
- data/lib/condo/version.rb +3 -0
- data/lib/tasks/condo_tasks.rake +4 -0
- data/test/condo_test.rb +27 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +59 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +25 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/test_helper.rb +15 -0
- 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
|
data/lib/condo/engine.rb
ADDED
@@ -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
|
data/lib/condo/errors.rb
ADDED
@@ -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
|
+
|