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 +114 -4
- data/app/assets/javascripts/condo/amazon.js +20 -6
- data/app/assets/javascripts/condo/controller.js +53 -8
- data/app/assets/javascripts/condo/google.js +10 -3
- data/app/assets/javascripts/condo/rackspace.js +12 -6
- data/app/assets/javascripts/condo/uploader.js +3 -0
- data/lib/condo.rb +3 -1
- data/lib/condo/configuration.rb +13 -2
- data/lib/condo/version.rb +1 -1
- metadata +2 -2
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
|
-
|
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
|
-
},
|
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
|
-
},
|
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('
|
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
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
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
|
-
},
|
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
|
-
},
|
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
|
-
},
|
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;
|
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
|
-
:
|
71
|
+
:location => upload.provider_location,
|
70
72
|
:upload => upload
|
71
73
|
})
|
72
74
|
|
data/lib/condo/configuration.rb
CHANGED
@@ -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 {
|
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
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
|
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-
|
12
|
+
date: 2012-11-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|