condo 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -1,11 +1,12 @@
1
- h1. Condominios
1
+ h1. Condominios aka Condo
2
2
 
3
- A Rails plugin that makes direct uploads to multiple cloud storage providers easy.
3
+ A "Rails plugin":http://guides.rubyonrails.org/plugins.html and "AngularJS application":http://angularjs.org/ that makes direct uploads to multiple cloud storage providers easy.
4
4
  Only supports "XMLHttpRequest Level 2":http://en.wikipedia.org/wiki/XMLHttpRequest capable browsers and cloud providers that have a "RESTful API":http://en.wikipedia.org/wiki/Representational_state_transfer with "CORS":http://en.wikipedia.org/wiki/Cross-origin_resource_sharing support.
5
5
 
6
6
  Why compromise?
7
7
 
8
- Get started now: @gem install condo@
8
+ Get started now: @gem install condo@ or checkout the "example application":https://github.com/cotag/condo_example
9
+ Also see our "github pages site":http://cotag.github.com/Condominios/
9
10
 
10
11
 
11
12
  h2. License
@@ -13,8 +14,117 @@ h2. License
13
14
  GNU Lesser General Public License v3 (LGPL version 3)
14
15
 
15
16
 
17
+ h2. Concept
18
+
19
+ Condominios was created to provide direct to cloud uploads using standards based browser technology. However it is not limited to that use case.
20
+ The API is RESTful, providing an abstraction layer and signed URLs that can be utilised in native (mobile) applications.
21
+
22
+ The main advantages are:
23
+ * Off-loads processing to client machines
24
+ * Better guarantees against upload corruption
25
+ ** file hashing on the client side instead of an intermediary where it probably won't be hashed either
26
+ * Upload results are guaranteed if the cloud provider provides atomic operations
27
+ ** user is always aware of any failures in the process
28
+ * Detailed progress and control over the upload
29
+
30
+ This has numerous advantages over traditional Form Data style post uploads too.
31
+ * Progress bars
32
+ * Resumability when uploading large files
33
+
34
+
35
+ Support for all major browsers and IE10.
36
+ * Tested in Firefox 4, Safari 6 and Chromes latest stable
37
+
38
+
16
39
  h2. Usage
17
40
 
18
- Coming Soon!
41
+ h3. Terms
42
+
43
+ * Residence == the current storage provider
44
+ * Resident == the current user
45
+
46
+
47
+ h3. Quick Start
48
+
49
+ See the "example application":https://github.com/cotag/condo_example which implements the steps below on an otherwise blank rails app.
50
+
51
+ # Add the following to your rails application gemfile:
52
+ #* @gem 'condo'@
53
+ #* @gem 'condo_active_record'@ (more backends coming soon)
54
+ #* @gem 'condo_interface'@ (optional)
55
+ # Run migrations
56
+ #* @rake railties:install:migrations FROM=condo_active_record@
57
+ #* @rake db:migrate@
58
+ # Create an initialiser for any default residencies. (details further down)
59
+ # Create controllers that will be used as Condo endpoints
60
+ #* Typically @rails g controller Uploads@
61
+ #* Add the resource to your routes
62
+ # At the top of the new controller add the following line to the class: @include Condo@
63
+ #* This creates the following public methods at run time: new, create, edit, update, destroy implementing the API
64
+ #* The following protected methods are also generated: set_residence, current_residence, current_resident, current_upload
65
+ # You are encouraged to use standard filters to authenticate users and set the residence (if this is dynamic) + implement index / show if desired
66
+ # You must implement the following call-backs:
67
+ #* resident_id - this should provide a unique identifier for the current user, used for authorisation
68
+ #* upload_complete - provides the upload information for storage in the greater application logic. Return true if successful.
69
+ #* destroy_upload - provides the upload information so that a scheduled task can be created to clean up the upload. Return true if successfully scheduled.
70
+ #** This should be done in the background using something like "Fog":http://fog.io/ Can't trust the client
71
+
72
+
73
+ If you are using "Condo Interface":https://github.com/cotag/condo_interface then you may want to do the following:
74
+ # Create an index for your controller @def index; end@
75
+ # Create an index.html.erb in your view with:
76
+ #* @<div class="uploads-container" data-ng-app="CondoUploader"><%= render "condo_interface/upload" %></div>@
77
+
78
+ Alternative you could load an AngularJS template linking to <%= asset_path('templates/_upload.html') %>
79
+
80
+
81
+ h3. Defining Static Residencies
82
+
83
+ If you are creating an application that only communicates with one or two storage providers or accounts then this is the simplest way to get started.
84
+ In an initialiser (<-- I'm Australian) do the following:
85
+
86
+ <pre><code class="ruby">
87
+ Condo::Configuration.add_residence(:AmazonS3, {
88
+ :access_id => ENV['S3_KEY'],
89
+ :secret_key => ENV['S3_SECRET']
90
+ # :location => 'us-west-1' # or 'ap-southeast-1' etc (see http://docs.amazonwebservices.com/general/latest/gr/rande.html#s3_region)
91
+ # Defaults to 'us-east-1' or US Standard - not required for Google
92
+ # :namespace => :admin_resident # Allows you to assign different defaults to different controllers
93
+ # Controller must have the following line 'set_namespace :admin_resident'
94
+ })
95
+
96
+ </code></pre>
97
+
98
+ The first residence to be defined in a namespace will be the default. To change the residence for the current request use @set_residence(:name, :location)@ - location is optional
99
+ Currently available residencies:
100
+ * :AmazonS3
101
+ * :GoogleCloudStorage
102
+ * :RackspaceCloudFiles
103
+
104
+
105
+ You can also define a dynamic residence each request (maybe clients provided you with access information for their storage provider)
106
+
107
+ <pre><code class="ruby">
108
+ set_residence(:AmazonS3, {
109
+ :access_id => user.s3_key,
110
+ :secret_key => user.s3_secret,
111
+ :dynamic => true # Otherwise the same as add_residence
112
+ });
113
+
114
+
115
+ </code></pre>
116
+
117
+
118
+ h3. Callbacks
119
+
120
+ These are pretty well defined "here":https://github.com/cotag/condo_example/blob/master/app/controllers/uploads_controller.rb
121
+
122
+
123
+ h2. TODO::
19
124
 
125
+ # Write tests... So many tests
126
+ # Create a wiki describing things in more detail
127
+ # Implement API for more residencies
128
+ # Sign other useful requests (bucket listings with search etc)
129
+ #* For Dropbox or Megaupload style applications
20
130
 
@@ -60,7 +60,10 @@
60
60
  var self = this,
61
61
  strategy = null,
62
62
  part_size = 5242880, // Multi-part uploads should be bigger then this
63
+ pausing = false,
63
64
  defaultError = function(reason) {
65
+ self.error = !pausing;
66
+ pausing = false;
64
67
  self.pause(reason);
65
68
  },
66
69
 
@@ -210,9 +213,7 @@
210
213
  set_part(data, result);
211
214
  }, defaultError);
212
215
 
213
- }, function(reason){
214
- self.pause(reason);
215
- }); // END BUILD_REQUEST
216
+ }, defaultError); // END BUILD_REQUEST
216
217
 
217
218
  } else {
218
219
  //
@@ -307,6 +308,15 @@
307
308
  this.message = 'pending';
308
309
  this.name = file.name;
309
310
  this.size = file.size;
311
+ this.error = false;
312
+
313
+
314
+ //
315
+ // File path is optional (amazon supports paths as part of the key name)
316
+ // http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/ListingKeysHierarchy.html
317
+ //
318
+ if(!!file.dir_path)
319
+ this.path = file.dir_path;
310
320
 
311
321
 
312
322
  //
@@ -318,6 +328,9 @@
318
328
 
319
329
  this.start = function(){
320
330
  if(strategy == null) { // We need to create the upload
331
+ self.error = false;
332
+ pausing = false;
333
+
321
334
  //
322
335
  // Update part size if required
323
336
  //
@@ -346,13 +359,13 @@
346
359
  }
347
360
  }, defaultError);
348
361
 
349
- }, function(reason){
350
- self.pause(reason);
351
- }); // END BUILD_REQUEST
362
+ }, defaultError); // END BUILD_REQUEST
352
363
 
353
364
 
354
365
  } else if (this.state == PAUSED) { // We need to resume the upload if it is paused
355
366
  this.message = null;
367
+ self.error = false;
368
+ pausing = false;
356
369
  strategy.resume();
357
370
  }
358
371
  };
@@ -360,6 +373,7 @@
360
373
  this.pause = function(reason) {
361
374
  if(strategy != null && this.state == UPLOADING) { // Check if the upload is uploading
362
375
  this.state = PAUSED;
376
+ pausing = true;
363
377
  strategy.pause();
364
378
  } else if (this.state <= STARTED) {
365
379
  this.state = PAUSED;
@@ -19,10 +19,10 @@
19
19
  (function (factory) {
20
20
  if (typeof define === 'function' && define.amd) {
21
21
  // AMD
22
- define(['jquery', 'condo_uploader'], factory);
22
+ define('condo_controller', ['jquery', 'condo_uploader'], factory);
23
23
  } else {
24
24
  // Browser globals
25
- factory(jQuery, window.CondoUploader);
25
+ window.CondoController = factory(jQuery, window.CondoUploader);
26
26
  }
27
27
  }(function ($, uploads, undefined) {
28
28
  'use strict';
@@ -58,11 +58,15 @@
58
58
 
59
59
 
60
60
 
61
- }]).controller('UploadsCtrl', ['$scope', 'Condo.Api', 'Condo.Broadcast', function($scope, api, broadcaster) {
61
+ }]).controller('Condo.Controller', ['$scope', 'Condo.Api', 'Condo.Broadcast', function($scope, api, broadcaster) {
62
62
 
63
63
  $scope.uploads = [];
64
+ $scope.upload_count = 0;
64
65
  $scope.endpoint = '/uploads'; // Default, the directive can overwrite this
66
+
65
67
  $scope.autostart = true;
68
+ $scope.ignore_errors = true; // Continue to autostart after an error
69
+ $scope.parallelism = 1; // number of uploads at once
66
70
 
67
71
 
68
72
  $scope.add = function(files) {
@@ -71,6 +75,11 @@
71
75
  ret = 0; // We only want to check for auto-start after the files have been added
72
76
 
73
77
  for (; i < length; i += 1) {
78
+ if(files[i].size <= 0 || files[i].type == '')
79
+ continue;
80
+
81
+ $scope.upload_count += 1;
82
+
74
83
  api.check_provider($scope.endpoint, files[i]).then(function(upload){
75
84
  ret += 1;
76
85
  $scope.uploads.push(upload);
@@ -78,6 +87,8 @@
78
87
  $scope.check_autostart();
79
88
  }, function(failure) {
80
89
 
90
+ $scope.upload_count -= 1;
91
+
81
92
  ret += 1;
82
93
  if(ret == length)
83
94
  $scope.check_autostart();
@@ -104,6 +115,7 @@
104
115
  for (var i = 0, length = $scope.uploads.length; i < length; i += 1) {
105
116
  if($scope.uploads[i] === upload) {
106
117
  $scope.uploads.splice(i, 1);
118
+ $scope.upload_count -= 1;
107
119
  break;
108
120
  }
109
121
  }
@@ -127,28 +139,57 @@
127
139
  });
128
140
 
129
141
 
142
+ //
143
+ // Autostart more uploads as this is bumped up
144
+ //
145
+ $scope.$watch('parallelism', function(newValue, oldValue) {
146
+ if(newValue > oldValue)
147
+ $scope.check_autostart();
148
+ });
149
+
150
+
130
151
  $scope.check_autostart = function() {
131
152
  //
132
153
  // Check if any uploads have been started already
133
154
  // If there are no active uploads we'll auto-start
134
155
  //
156
+ // PENDING = 0,
157
+ // STARTED = 1,
158
+ // PAUSED = 2,
159
+ // UPLOADING = 3,
160
+ // COMPLETED = 4,
161
+ // ABORTED = 5
162
+ //
135
163
  if ($scope.autostart) {
136
164
  var shouldStart = true,
137
- state, i, length;
165
+ state, i, length, started = 0;
138
166
 
139
167
  for (i = 0, length = $scope.uploads.length; i < length; i += 1) {
140
168
  state = $scope.uploads[i].state;
141
- if (state > 0 && state < 4) {
142
- shouldStart = false;
143
- break;
169
+
170
+ //
171
+ // Count started uploads (that don't have errors if we are ignoring errors)
172
+ // Up until we've reached our parallel limit, then stop
173
+ //
174
+ if (state > 0 && state < 4 && !($scope.uploads[i].error && $scope.ignore_errors)) {
175
+ started += 1;
176
+ if(started >= $scope.parallelism) {
177
+ shouldStart = false;
178
+ break;
179
+ }
144
180
  }
145
181
  }
146
182
 
147
183
  if (shouldStart) {
184
+ started = $scope.parallelism - started; // How many can we start
185
+
148
186
  for (i = 0; i < length; i += 1) {
149
187
  if ($scope.uploads[i].state == 0) {
150
188
  $scope.uploads[i].start();
151
- break;
189
+
190
+ started -= 1;
191
+ if(started <= 0) // Break if we can't start anymore
192
+ break;
152
193
  }
153
194
  }
154
195
  }
@@ -158,5 +199,9 @@
158
199
  }]);
159
200
 
160
201
 
202
+ //
203
+ // Anonymous function return
204
+ //
205
+ return uploads;
161
206
 
162
207
  }));
@@ -60,7 +60,10 @@
60
60
  var self = this,
61
61
  strategy = null,
62
62
  part_size = 1048576, // This is the amount of the file we read into memory as we are building the hash (1mb)
63
+ pausing = false,
63
64
  defaultError = function(reason) {
65
+ self.error = !pausing;
66
+ pausing = false;
64
67
  self.pause(reason);
65
68
  },
66
69
 
@@ -200,6 +203,7 @@
200
203
  this.message = 'pending';
201
204
  this.name = file.name;
202
205
  this.size = file.size;
206
+ this.error = false;
203
207
 
204
208
 
205
209
  //
@@ -212,6 +216,8 @@
212
216
  this.start = function(){
213
217
  if(strategy == null) { // We need to create the upload
214
218
 
219
+ this.error = false;
220
+ pausing = false;
215
221
  this.message = null;
216
222
  this.state = STARTED;
217
223
  strategy = {}; // This function shouldn't be called twice so we need a state
@@ -229,12 +235,12 @@
229
235
  }
230
236
  }, defaultError);
231
237
 
232
- }, function(reason){
233
- self.pause(reason);
234
- }); // END BUILD_REQUEST
238
+ }, defaultError); // END BUILD_REQUEST
235
239
 
236
240
 
237
241
  } else if (this.state == PAUSED) { // We need to resume the upload if it is paused
242
+ this.error = false;
243
+ pausing = false;
238
244
  this.message = null;
239
245
  strategy.resume();
240
246
  }
@@ -243,6 +249,7 @@
243
249
  this.pause = function(reason) {
244
250
  if(strategy != null && this.state == UPLOADING) { // Check if the upload is uploading
245
251
  this.state = PAUSED;
252
+ pausing = true;
246
253
  strategy.pause();
247
254
  } else if (this.state <= STARTED) {
248
255
  this.state = PAUSED;
@@ -44,7 +44,10 @@
44
44
  var self = this,
45
45
  strategy = null,
46
46
  part_size = 2097152, // Multi-part uploads should be bigger then this
47
+ pausing = false,
47
48
  defaultError = function(reason) {
49
+ self.error = !pausing;
50
+ pausing = false;
48
51
  self.pause(reason);
49
52
  },
50
53
 
@@ -184,9 +187,7 @@
184
187
  set_part(data, result);
185
188
  }, defaultError);
186
189
 
187
- }, function(reason){
188
- self.pause(reason);
189
- }); // END BUILD_REQUEST
190
+ }, defaultError); // END BUILD_REQUEST
190
191
 
191
192
  } else {
192
193
  //
@@ -248,6 +249,7 @@
248
249
  this.message = 'pending';
249
250
  this.name = file.name;
250
251
  this.size = file.size;
252
+ this.error = false;
251
253
 
252
254
 
253
255
  //
@@ -260,6 +262,8 @@
260
262
  this.start = function(){
261
263
  if(strategy == null) { // We need to create the upload
262
264
 
265
+ pausing = false;
266
+ this.error = false;
263
267
  this.message = null;
264
268
  this.state = STARTED;
265
269
  strategy = {}; // This function shouldn't be called twice so we need a state (TODO:: fix this)
@@ -277,12 +281,13 @@
277
281
  }
278
282
  }, defaultError);
279
283
 
280
- }, function(reason){
281
- self.pause(reason);
282
- }); // END BUILD_REQUEST
284
+ }, defaultError); // END BUILD_REQUEST
283
285
 
284
286
 
285
287
  } else if (this.state == PAUSED) { // We need to resume the upload if it is paused
288
+
289
+ pausing = false;
290
+ this.error = false;
286
291
  this.message = null;
287
292
  strategy.resume();
288
293
  }
@@ -291,6 +296,7 @@
291
296
  this.pause = function(reason) {
292
297
  if(strategy != null && this.state == UPLOADING) { // Check if the upload is uploading
293
298
  this.state = PAUSED;
299
+ pausing = true;
294
300
  strategy.pause();
295
301
  } else if (this.state <= STARTED) {
296
302
  this.state = PAUSED;
@@ -251,6 +251,9 @@
251
251
  params['file_size'] = the_file.size;
252
252
  params['file_name'] = the_file.name;
253
253
 
254
+ if(!!the_file.dir_path)
255
+ params['file_path'] = the_file.dir_path;
256
+
254
257
  return $http({
255
258
  method: 'GET',
256
259
  url: api_endpoint + '/new',
data/lib/condo.rb CHANGED
@@ -29,6 +29,7 @@ module Condo
29
29
  @upload ||= {}
30
30
  @upload[:file_size] = params[:file_size].to_i
31
31
  @upload[:file_name] = (instance_eval &@@callbacks[:sanitize_filename])
32
+ @upload[:file_path] = (instance_eval &@@callbacks[:sanitize_filepath]) if params[:file_path]
32
33
 
33
34
  valid, errors = instance_eval &@@callbacks[:pre_validation] # Ensure the upload request is valid before uploading
34
35
 
@@ -56,6 +57,7 @@ module Condo
56
57
  @upload[:file_size] = params[:file_size].to_i
57
58
  @upload[:file_id] = params[:file_id]
58
59
  @upload[:file_name] = (instance_eval &@@callbacks[:sanitize_filename])
60
+ @upload[:file_path] = (instance_eval &@@callbacks[:sanitize_filepath]) if params[:file_path]
59
61
 
60
62
  upload = condo_backend.check_exists({
61
63
  :user_id => resident,
@@ -66,7 +68,7 @@ module Condo
66
68
 
67
69
  if upload.present?
68
70
  residence = set_residence(upload.provider_name, {
69
- :provider_location => upload.provider_location,
71
+ :location => upload.provider_location,
70
72
  :upload => upload
71
73
  })
72
74
 
@@ -8,7 +8,13 @@ module Condo
8
8
  @@callbacks = {
9
9
  #:resident_id # Must be defined by the including class
10
10
  :bucket_name => proc {"#{Rails.application.class.parent_name}#{instance_eval @@callbacks[:resident_id]}"},
11
- :object_key => proc {params[:file_name]},
11
+ :object_key => proc {
12
+ if params[:file_path]
13
+ params[:file_path] + params[:file_name]
14
+ else
15
+ params[:file_name]
16
+ end
17
+ },
12
18
  :object_options => proc {{:permissions => :private}},
13
19
  :pre_validation => proc {true}, # To respond with errors use: lambda {return false, {:errors => {:param_name => 'wtf are you doing?'}}}
14
20
  :sanitize_filename => proc {
@@ -16,6 +22,11 @@ module Condo
16
22
  filename.gsub!(/^.*(\\|\/)/, '') # get only the filename (just in case)
17
23
  filename.gsub!(/[^\w\.\-]/,'_') # replace all non alphanumeric or periods with underscore
18
24
  end
25
+ },
26
+ :sanitize_filepath => proc {
27
+ params[:file_path].tap do |filepath|
28
+ filepath.gsub!(/[^\w\.\-\/]/,'_') # replace all non alphanumeric or periods with underscore
29
+ end
19
30
  }
20
31
  #:upload_complete # Must be defined by the including class
21
32
  #:destroy_upload # the actual delete should be done by the application
@@ -76,7 +87,7 @@ module Condo
76
87
  @@locations[namespace][name][options[:location].to_sym] = res
77
88
  else
78
89
  @@locations[namespace][name][:default] = res
79
- @@locations[namespace][name][res.location] = res
90
+ @@locations[namespace][name][res.location.to_sym] = res
80
91
  end
81
92
  end
82
93
  end
data/lib/condo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Condo
2
- VERSION = "0.0.1"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: condo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-15 00:00:00.000000000 Z
12
+ date: 2012-11-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails