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,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