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