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,298 @@
1
+ /**
2
+ * CoTag Condo
3
+ * Direct to cloud resumable uploads
4
+ *
5
+ * Copyright (c) 2012 CoTag Media.
6
+ *
7
+ * @author Stephen von Takach <steve@cotag.me>
8
+ * @copyright 2012 cotag.me
9
+ *
10
+ *
11
+ * References:
12
+ * * https://github.com/umdjs/umd
13
+ * * https://github.com/addyosmani/jquery-plugin-patterns
14
+ * * http://docs.angularjs.org/api/ng.$http
15
+ * * http://docs.angularjs.org/api/ng.$q
16
+ *
17
+ **/
18
+
19
+ (function (factory) {
20
+ if (typeof define === 'function' && define.amd) {
21
+ // AMD
22
+ define('condo_uploader', ['jquery'], factory);
23
+ } else {
24
+ // Browser globals
25
+ window.CondoUploader = factory(jQuery);
26
+ }
27
+ }(function ($, undefined) {
28
+ 'use strict';
29
+
30
+ var uploads = angular.module('CondoUploader', []);
31
+
32
+
33
+ //
34
+ // Implements the Condo API
35
+ //
36
+ uploads.factory('Condo.Api', ['$http', '$rootScope', '$q', 'Condo.AmazonS3', 'Condo.RackspaceCloudFiles', 'Condo.GoogleCloudStorage', function($http, $rootScope, $q, AmazonS3Condo, RackspaceFilesCondo, GoogleStorageCondo) {
37
+
38
+
39
+ var token = $('meta[name="csrf-token"]').attr('content'),
40
+ residencies = {
41
+ AmazonS3: AmazonS3Condo,
42
+ RackspaceCloudFiles: RackspaceFilesCondo,
43
+ GoogleCloudStorage: GoogleStorageCondo
44
+ },
45
+
46
+
47
+ condoConnection = function(api_endpoint, params) {
48
+ this.endpoint = api_endpoint; // The API mounting point
49
+ this.params = params; // Custom API parameters
50
+
51
+ this.upload_id = null; // The current upload ID
52
+ this.aborting = false; // Has the user has requested an abort?
53
+ this.xhr = null; // Any active cloud file xhr requests
54
+ };
55
+
56
+
57
+ $http.defaults.headers = {};
58
+ $http.defaults.headers['common'] = {'X-Requested-With': 'XMLHttpRequest'};
59
+ $http.defaults.headers['post'] = {'X-CSRF-Token': token};
60
+ $http.defaults.headers['put'] = {'X-CSRF-Token': token};
61
+ $http.defaults.headers['delete'] = {'X-CSRF-Token': token};
62
+
63
+ condoConnection.prototype = {
64
+
65
+
66
+ //
67
+ // Creates an entry in the database for the requested file and returns the upload signature
68
+ // If an entry already exists it returns a parts request signature for resumable uploads
69
+ //
70
+ create: function(options) { // file_id: 123, options: {}
71
+ var self = this;
72
+ options = options || {};
73
+ this.aborting = false;
74
+
75
+ if(!!options['file_id'])
76
+ this.params['file_id'] = options['file_id'];
77
+
78
+ if(!!options['parameters'])
79
+ this.params['parameters'] = options['parameters']; // We may be requesting the next set of parts
80
+
81
+ return $http({
82
+ method: 'POST',
83
+ url: this.endpoint,
84
+ params: this.params
85
+ }).then(function(result){
86
+ result = result.data;
87
+ self.upload_id = result.upload_id; // Extract the upload id from the results
88
+
89
+ if (!self.aborting)
90
+ return result;
91
+ else
92
+ return $q.reject(undefined);
93
+ }, function(reason) {
94
+ return $q.reject('upload error');
95
+ });
96
+ },
97
+
98
+
99
+ //
100
+ // This requests a chunk signature
101
+ // Only used for resumable uploads
102
+ //
103
+ edit: function(part_number, part_id) {
104
+ var self = this;
105
+ this.aborting = false;
106
+
107
+ return $http({
108
+ method: 'GET',
109
+ url: this.endpoint + '/' + this.upload_id + '/edit',
110
+ params: {
111
+ part: part_number,
112
+ file_id: part_id
113
+ }
114
+ }).then(function(result){
115
+ if (!self.aborting)
116
+ return result.data;
117
+ else
118
+ return $q.reject(undefined);
119
+ }, function(reason) {
120
+ return $q.reject('upload error');
121
+ });
122
+ },
123
+
124
+
125
+ //
126
+ // If resumable id is present the upload is updated
127
+ // Otherwise the upload deemed complete
128
+ //
129
+ update: function(params) { // optional parameters (resumable_id, file_id and part)
130
+ var self = this;
131
+
132
+ this.aborting = false;
133
+ params = params || {};
134
+
135
+ return $http({
136
+ method: 'PUT',
137
+ url: this.endpoint + '/' + this.upload_id,
138
+ params: params
139
+ }).then(function(result){
140
+ if (!self.aborting)
141
+ return result.data;
142
+ else
143
+ return $q.reject(undefined);
144
+ }, function(reason) {
145
+ if (reason.status == 401 && params.resumable_id == undefined) {
146
+ return ''; // User may have paused upload as put was being sent. We should let this through just to update the UI
147
+ } else
148
+ return $q.reject('upload error');
149
+ });
150
+ },
151
+
152
+
153
+ //
154
+ // Cancels a resumable upload
155
+ // The actual destruction of the file is handled on the server side as we can't trust the client to do this
156
+ // We don't care if this succeeds as the back-end will destroy the file eventually anyway.
157
+ //
158
+ destroy: function() {
159
+ return $http({
160
+ method: 'DELETE',
161
+ url: this.endpoint + '/' + this.upload_id
162
+ });
163
+ },
164
+
165
+
166
+
167
+ //
168
+ // Provides a promise for any request this is what communicated with the cloud storage servers
169
+ //
170
+ process_request: function(signature, progress_callback) {
171
+ var self = this,
172
+ result = $q.defer(),
173
+ params = {
174
+ url: signature.signature.url,
175
+ type: signature.signature.verb,
176
+ headers: signature.signature.headers,
177
+ processData: false,
178
+ success: function(response, textStatus, jqXHR) {
179
+ self.xhr = null;
180
+ result.resolve(response);
181
+ },
182
+ error: function(jqXHR, textStatus, errorThrown) {
183
+ self.xhr = null;
184
+ if (!self.aborting)
185
+ result.reject('upload error');
186
+ else
187
+ result.reject(undefined);
188
+ },
189
+ complete: function(jqXHR, textStatus) {
190
+ if(!$rootScope.$$phase) {
191
+ $rootScope.$apply(); // This triggers the promise response
192
+ }
193
+ }
194
+ };
195
+
196
+ this.aborting = false;
197
+
198
+ if (!!self.xhr) {
199
+ result.reject('request in progress'); // This is awesome
200
+ return result.promise;
201
+ }
202
+
203
+ if(!!signature.data){
204
+ params['data'] = signature.data;
205
+ }
206
+
207
+ if(!!progress_callback) {
208
+ params['xhr'] = function() {
209
+ var xhr = $.ajaxSettings.xhr();
210
+ if(!!xhr.upload){
211
+ xhr.upload.addEventListener('progress', function(e) {
212
+ if (e.lengthComputable) {
213
+ var phase = $rootScope.$$phase;
214
+ if(phase == '$apply' || phase == '$digest') {
215
+ progress_callback(e.loaded);
216
+ } else {
217
+ $rootScope.$apply(function(){
218
+ progress_callback(e.loaded);
219
+ });
220
+ }
221
+ }
222
+ }, false);
223
+ }
224
+ return xhr;
225
+ };
226
+ }
227
+
228
+ this.xhr = $.ajax(params);
229
+
230
+ return result.promise;
231
+ },
232
+
233
+
234
+ //
235
+ // Will trigger the error call-back of the xhr object
236
+ //
237
+ abort: function() {
238
+ this.aborting = true;
239
+ if(!!this.xhr) {
240
+ this.xhr.abort();
241
+ }
242
+ }
243
+ };
244
+
245
+ return {
246
+ //
247
+ // Used to determine what upload strategy to use (Amazon, Google, etc)
248
+ //
249
+ check_provider: function(api_endpoint, the_file, params) {
250
+ params = params || {};
251
+ params['file_size'] = the_file.size;
252
+ params['file_name'] = the_file.name;
253
+
254
+ return $http({
255
+ method: 'GET',
256
+ url: api_endpoint + '/new',
257
+ params: params
258
+ }).then(function(result){
259
+ if(!!residencies[result.data.residence]) {
260
+
261
+ var api = new condoConnection(api_endpoint, params);
262
+
263
+ //
264
+ // TODO:: Check if a file is already in the list and reject if it is
265
+ //
266
+ return residencies[result.data.residence].new_upload(api, the_file); // return the instantiated provider
267
+
268
+ } else {
269
+ return $q.reject({
270
+ reason: 'storage provider not found'
271
+ });
272
+ }
273
+ }, function(reason) {
274
+ if(reason.status == 406) {
275
+ return $q.reject({
276
+ reason: 'file not accepted',
277
+ details: reason.data,
278
+ file: the_file
279
+ });
280
+ } else {
281
+ return $q.reject({
282
+ reason: 'server error',
283
+ file: the_file
284
+ });
285
+ }
286
+ });
287
+ }
288
+ };
289
+ }]);
290
+
291
+
292
+
293
+ //
294
+ // Anonymous function return
295
+ //
296
+ return uploads;
297
+
298
+ }));
@@ -0,0 +1,267 @@
1
+ require 'condo/engine'
2
+ require 'condo/errors'
3
+ require 'condo/configuration'
4
+
5
+
6
+ #Dir[File.join('condo', 'strata', '*.rb')].each do |file| # Using autoload_paths now
7
+ # require file[0..-4] # Removes the .rb ext name
8
+ #end
9
+
10
+
11
+ module Condo
12
+
13
+ #
14
+ # TODO:: Simplify the parameters passed in
15
+ # Object options should be set at the application level
16
+ # The application can set these based on the custom params.
17
+ # Have an instance member that holds the parameter set: @upload
18
+ #
19
+ def self.included(base)
20
+ base.class_eval do
21
+
22
+
23
+ def new
24
+ #
25
+ # Returns the provider that will be used for this file upload
26
+ #
27
+ resident = current_resident
28
+
29
+ @upload ||= {}
30
+ @upload[:file_size] = params[:file_size].to_i
31
+ @upload[:file_name] = (instance_eval &@@callbacks[:sanitize_filename])
32
+
33
+ valid, errors = instance_eval &@@callbacks[:pre_validation] # Ensure the upload request is valid before uploading
34
+
35
+ if !!valid
36
+ set_residence(nil, {:resident => resident, :params => @upload}) if condo_config.dynamic_provider_present?(@@namespace)
37
+ residence = current_residence
38
+
39
+ render :json => {:residence => residence.name}
40
+
41
+ elsif errors.is_a? Hash
42
+ render :json => errors, :status => :not_acceptable
43
+ else
44
+ render :nothing => true, :status => :not_acceptable
45
+ end
46
+ end
47
+
48
+ def create
49
+ #
50
+ # Check for existing upload or create a new one
51
+ # => mutually exclusive so can send back either the parts signature from show or a bucket creation signature and the upload_id
52
+ #
53
+ resident = current_resident
54
+
55
+ @upload = {}
56
+ @upload[:file_size] = params[:file_size].to_i
57
+ @upload[:file_id] = params[:file_id]
58
+ @upload[:file_name] = (instance_eval &@@callbacks[:sanitize_filename])
59
+
60
+ upload = condo_backend.check_exists({
61
+ :user_id => resident,
62
+ :file_name => @upload[:file_name],
63
+ :file_size => @upload[:file_size],
64
+ :file_id => @upload[:file_id]
65
+ })
66
+
67
+ if upload.present?
68
+ residence = set_residence(upload.provider_name, {
69
+ :provider_location => upload.provider_location,
70
+ :upload => upload
71
+ })
72
+
73
+ #
74
+ # Return the parts or direct upload sig
75
+ #
76
+ request = nil
77
+ if upload.resumable_id.present? && upload.resumable
78
+ upload.object_options[:parameters] = {} || params[:parameters] # May need to request the next set of parts
79
+ request = residence.get_parts({
80
+ :bucket_name => upload.bucket_name,
81
+ :object_key => upload.object_key,
82
+ :object_options => upload.object_options,
83
+ :resumable_id => upload.resumable_id
84
+ })
85
+ else
86
+ request = residence.new_upload({
87
+ :bucket_name => upload.bucket_name,
88
+ :object_key => upload.object_key,
89
+ :object_options => upload.object_options,
90
+ :file_size => upload.file_size,
91
+ :file_id => upload.file_id
92
+ })
93
+ end
94
+
95
+ render :json => request.merge(:upload_id => upload.id, :residence => residence.name)
96
+ else
97
+ #
98
+ # Create a new upload
99
+ #
100
+ valid, errors = instance_eval &@@callbacks[:pre_validation] # Ensure the upload request is valid before uploading
101
+
102
+
103
+ if !!valid
104
+ set_residence(nil, {:resident => resident, :params => @upload}) if condo_config.dynamic_provider_present?(@@namespace)
105
+ residence = current_residence
106
+
107
+ #
108
+ # Build the request
109
+ #
110
+ request = residence.new_upload(@upload.merge!({
111
+ :bucket_name => (instance_eval &@@callbacks[:bucket_name]), # Allow the application to define a custom bucket name
112
+ :object_key => (instance_eval &@@callbacks[:object_key]), # The object key should also be generated by the application
113
+ :object_options => (instance_eval &@@callbacks[:object_options]) # Do we want to mess with any of the options?
114
+ }))
115
+ resumable = request[:type] == :chunked_upload
116
+
117
+ #
118
+ # Save a reference to this upload in the database
119
+ # => This should throw an error on failure
120
+ #
121
+ upload = condo_backend.add_entry(@upload.merge!({:user_id => resident, :provider_name => residence.name, :provider_location => residence.location, :resumable => resumable}))
122
+ render :json => request.merge!(:upload_id => upload.id, :residence => residence.name)
123
+
124
+ elsif errors.is_a? Hash
125
+ render :json => errors, :status => :not_acceptable
126
+ else
127
+ render :nothing => true, :status => :not_acceptable
128
+ end
129
+ end
130
+ end
131
+
132
+
133
+ #
134
+ # Authorisation check all of these
135
+ #
136
+ def edit
137
+ #
138
+ # Get the signature for parts + final commit
139
+ #
140
+ upload = current_upload
141
+
142
+ if upload.resumable_id.present? && upload.resumable
143
+ residence = set_residence(upload.provider_name, {:location => upload.provider_location, :upload => upload})
144
+
145
+ request = residence.set_part({
146
+ :bucket_name => upload.bucket_name,
147
+ :object_key => upload.object_key,
148
+ :object_options => upload.object_options,
149
+ :resumable_id => upload.resumable_id,
150
+ :part => params[:part], # part may be called 'finish' for commit signature
151
+ :file_id => params[:file_id]
152
+ })
153
+
154
+ render :json => request.merge!(:upload_id => upload.id)
155
+ else
156
+ render :nothing => true, :status => :not_acceptable
157
+ end
158
+ end
159
+
160
+
161
+ def update
162
+ #
163
+ # Provide the upload id after creating a resumable upload (may not be completed)
164
+ # => We then provide the first part signature
165
+ #
166
+ # OR
167
+ #
168
+ # Complete an upload
169
+ #
170
+ if params[:resumable_id]
171
+ upload = current_upload
172
+ if upload.resumable
173
+ @current_upload = upload.update_entry :resumable_id => params[:resumable_id]
174
+ edit
175
+ else
176
+ render :nothing => true, :status => :not_acceptable
177
+ end
178
+ else
179
+ response = instance_exec current_upload, &@@callbacks[:upload_complete]
180
+ if !!response
181
+ current_upload.remove_entry
182
+ render :nothing => true
183
+ else
184
+ render :nothing => true, :status => :not_acceptable
185
+ end
186
+ end
187
+ end
188
+
189
+
190
+ def destroy
191
+ #
192
+ # Delete the file from the cloud system - the client is not responsible for this
193
+ #
194
+ response = instance_exec current_upload, &@@callbacks[:destroy_upload]
195
+ if !!response
196
+ current_upload.remove_entry
197
+ render :nothing => true
198
+ else
199
+ render :nothing => true, :status => :not_acceptable
200
+ end
201
+ end
202
+
203
+
204
+ protected
205
+
206
+
207
+ #
208
+ # A before filter can be used to select the cloud provider for the current user
209
+ # Otherwise the dynamic residence can be used when users are define their own storage locations
210
+ #
211
+ def set_residence(name, options = {})
212
+ options[:namespace] = @@namespace
213
+ @current_residence = condo_config.set_residence(name, options)
214
+ end
215
+
216
+ def current_residence
217
+ @current_residence ||= condo_config.residencies[0]
218
+ end
219
+
220
+ def current_upload
221
+ @current_upload ||= condo_backend.check_exists({:user_id => current_resident, :upload_id => (params[:upload_id] || params[:id])}).tap do |object| #current_residence.name && current_residence.location && resident.id.exists?
222
+ raise Condo::Errors::NotYourPlace unless object.present?
223
+ end
224
+ end
225
+
226
+ def current_resident
227
+ @current_resident ||= (instance_eval &@@callbacks[:resident_id]).tap do |object| # instance_exec for params
228
+ raise Condo::Errors::LostTheKeys unless object.present?
229
+ end
230
+ end
231
+
232
+ def condo_backend
233
+ Condo::Store
234
+ end
235
+
236
+ def condo_config
237
+ Condo::Configuration.instance
238
+ end
239
+
240
+
241
+ #
242
+ # Defines the default callbacks
243
+ #
244
+ (@@callbacks ||= {}).merge! Condo::Configuration.callbacks
245
+ @@namespace ||= :global
246
+
247
+
248
+ def self.set_callback(name, callback = nil, &block)
249
+ if callback.is_a?(Proc)
250
+ @@callbacks[name.to_sym] = callback
251
+ elsif block.present?
252
+ @@callbacks[name.to_sym] = block
253
+ else
254
+ raise ArgumentError, 'Condo callbacks must be defined with a Proc or Proc (lamba) object present'
255
+ end
256
+ end
257
+
258
+
259
+ def self.set_namespace(name)
260
+ @@namespace = name.to_sym
261
+ end
262
+
263
+ end
264
+ end
265
+
266
+
267
+ end