condo 0.0.1 → 1.0.0

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