cloudkit 0.10.1 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +11 -0
- data/README +7 -6
- data/Rakefile +13 -6
- data/TODO +7 -5
- data/cloudkit.gemspec +23 -23
- data/doc/curl.html +2 -2
- data/doc/index.html +4 -6
- data/examples/5.ru +2 -3
- data/examples/TOC +1 -3
- data/lib/cloudkit.rb +17 -10
- data/lib/cloudkit/constants.rb +0 -6
- data/lib/cloudkit/exceptions.rb +10 -0
- data/lib/cloudkit/flash_session.rb +1 -3
- data/lib/cloudkit/oauth_filter.rb +8 -16
- data/lib/cloudkit/oauth_store.rb +9 -13
- data/lib/cloudkit/openid_filter.rb +25 -7
- data/lib/cloudkit/openid_store.rb +14 -17
- data/lib/cloudkit/request.rb +6 -1
- data/lib/cloudkit/service.rb +15 -15
- data/lib/cloudkit/store.rb +97 -284
- data/lib/cloudkit/store/memory_table.rb +105 -0
- data/lib/cloudkit/store/resource.rb +256 -0
- data/lib/cloudkit/store/response_helpers.rb +0 -1
- data/lib/cloudkit/uri.rb +88 -0
- data/lib/cloudkit/user_store.rb +7 -14
- data/spec/ext_spec.rb +76 -0
- data/spec/flash_session_spec.rb +20 -0
- data/spec/memory_table_spec.rb +86 -0
- data/spec/oauth_filter_spec.rb +326 -0
- data/spec/oauth_store_spec.rb +10 -0
- data/spec/openid_filter_spec.rb +64 -0
- data/spec/openid_store_spec.rb +101 -0
- data/spec/rack_builder_spec.rb +39 -0
- data/spec/request_spec.rb +185 -0
- data/spec/resource_spec.rb +291 -0
- data/spec/service_spec.rb +974 -0
- data/{test/helper.rb → spec/spec_helper.rb} +14 -2
- data/spec/store_spec.rb +10 -0
- data/spec/uri_spec.rb +93 -0
- data/spec/user_store_spec.rb +10 -0
- data/spec/util_spec.rb +11 -0
- metadata +37 -61
- data/examples/6.ru +0 -10
- data/lib/cloudkit/store/adapter.rb +0 -8
- data/lib/cloudkit/store/extraction_view.rb +0 -57
- data/lib/cloudkit/store/sql_adapter.rb +0 -36
- data/test/ext_test.rb +0 -76
- data/test/flash_session_test.rb +0 -22
- data/test/oauth_filter_test.rb +0 -331
- data/test/oauth_store_test.rb +0 -12
- data/test/openid_filter_test.rb +0 -60
- data/test/openid_store_test.rb +0 -12
- data/test/rack_builder_test.rb +0 -41
- data/test/request_test.rb +0 -197
- data/test/service_test.rb +0 -971
- data/test/store_test.rb +0 -93
- data/test/user_store_test.rb +0 -12
- data/test/util_test.rb +0 -13
data/lib/cloudkit/oauth_store.rb
CHANGED
@@ -1,42 +1,38 @@
|
|
1
1
|
module CloudKit
|
2
2
|
|
3
3
|
# An OAuthStore is a thin abstraction around CloudKit::Store, providing
|
4
|
-
# consistent collection names, and allowing automatic
|
4
|
+
# consistent collection names, and allowing automatic upgrades in later
|
5
5
|
# releases if needed.
|
6
6
|
class OAuthStore
|
7
7
|
@@store = nil
|
8
8
|
|
9
9
|
# Initialize a Store for use with OAuth middleware. Load the static consumer
|
10
10
|
# resource if it does not exist.
|
11
|
-
def initialize
|
11
|
+
def initialize
|
12
12
|
@@store = Store.new(
|
13
13
|
:collections => [
|
14
14
|
:cloudkit_oauth_nonces,
|
15
15
|
:cloudkit_oauth_tokens,
|
16
16
|
:cloudkit_oauth_request_tokens,
|
17
|
-
:cloudkit_oauth_consumers]
|
18
|
-
|
17
|
+
:cloudkit_oauth_consumers]
|
18
|
+
) unless @@store
|
19
19
|
load_static_consumer
|
20
20
|
end
|
21
21
|
|
22
22
|
def get(uri, options={}) #:nodoc:
|
23
|
-
@@store.get(uri, options)
|
23
|
+
@@store.get(CloudKit::URI.new(uri), options)
|
24
24
|
end
|
25
25
|
|
26
26
|
def post(uri, options={}) #:nodoc:
|
27
|
-
@@store.post(uri, options)
|
27
|
+
@@store.post(CloudKit::URI.new(uri), options)
|
28
28
|
end
|
29
29
|
|
30
30
|
def put(uri, options={}) #:nodoc:
|
31
|
-
@@store.put(uri, options)
|
31
|
+
@@store.put(CloudKit::URI.new(uri), options)
|
32
32
|
end
|
33
33
|
|
34
34
|
def delete(uri, options={}) #:nodoc:
|
35
|
-
@@store.delete(uri, options)
|
36
|
-
end
|
37
|
-
|
38
|
-
def reset! #:nodoc:
|
39
|
-
@@store.reset!
|
35
|
+
@@store.delete(CloudKit::URI.new(uri), options)
|
40
36
|
end
|
41
37
|
|
42
38
|
# Return the version number for this store.
|
@@ -46,7 +42,7 @@ module CloudKit
|
|
46
42
|
# See the OAuth Discovery spec for more info on static consumers.
|
47
43
|
def load_static_consumer
|
48
44
|
json = JSON.generate(:secret => '')
|
49
|
-
@@store.put('/cloudkit_oauth_consumers/cloudkitconsumer', :json => json)
|
45
|
+
@@store.put(CloudKit::URI.new('/cloudkit_oauth_consumers/cloudkitconsumer'), :json => json)
|
50
46
|
end
|
51
47
|
end
|
52
48
|
end
|
@@ -3,6 +3,11 @@ module CloudKit
|
|
3
3
|
# An OpenIDFilter provides OpenID authentication, listening for upstream
|
4
4
|
# OAuth authentication and bypassing if already authorized.
|
5
5
|
#
|
6
|
+
# The root URI, "/", is always bypassed. More URIs can also be bypassed using
|
7
|
+
# the :allow option:
|
8
|
+
#
|
9
|
+
# use OpenIDFilter, :allow => ['/foo', '/bar']
|
10
|
+
#
|
6
11
|
# Responds to the following URIs:
|
7
12
|
# /login
|
8
13
|
# /logout
|
@@ -15,14 +20,14 @@ module CloudKit
|
|
15
20
|
@@store = nil
|
16
21
|
|
17
22
|
def initialize(app, options={})
|
18
|
-
@app
|
23
|
+
@app = app
|
24
|
+
@options = options
|
19
25
|
end
|
20
26
|
|
21
27
|
def call(env)
|
22
28
|
@@lock.synchronize do
|
23
|
-
@@store = OpenIDStore.new
|
24
|
-
@users = UserStore.new
|
25
|
-
@@store.get_association('x') rescue nil # refresh sqlite3
|
29
|
+
@@store = OpenIDStore.new
|
30
|
+
@users = UserStore.new
|
26
31
|
end unless @@store
|
27
32
|
|
28
33
|
request = Request.new(env)
|
@@ -34,7 +39,7 @@ module CloudKit
|
|
34
39
|
when r(:get, '/openid_complete'); complete_openid_login(request)
|
35
40
|
when r(:post, request.logout_url); logout(request)
|
36
41
|
else
|
37
|
-
if
|
42
|
+
if bypass?(request)
|
38
43
|
@app.call(env)
|
39
44
|
else
|
40
45
|
if request.env[CLOUDKIT_AUTH_CHALLENGE]
|
@@ -103,7 +108,8 @@ module CloudKit
|
|
103
108
|
end
|
104
109
|
|
105
110
|
result = @users.get(
|
106
|
-
'/
|
111
|
+
'/cloudkit_users',
|
112
|
+
# '/cloudkit_login_view',
|
107
113
|
:identity_url => idp_response.endpoint.claimed_id)
|
108
114
|
user_uris = result.parsed_content['uris']
|
109
115
|
|
@@ -183,7 +189,8 @@ module CloudKit
|
|
183
189
|
def valid_remember_me_token?(request)
|
184
190
|
return false unless token = request.cookies['remember_me']
|
185
191
|
|
186
|
-
result = @users.get('/cloudkit_login_view', :remember_me_token => token)
|
192
|
+
# result = @users.get('/cloudkit_login_view', :remember_me_token => token)
|
193
|
+
result = @users.get('/cloudkit_users', :remember_me_token => token)
|
187
194
|
return false unless result.status == 200
|
188
195
|
|
189
196
|
user_uris = result.parsed_content['uris']
|
@@ -204,5 +211,16 @@ module CloudKit
|
|
204
211
|
def two_weeks_from_now
|
205
212
|
Time.now.to_i+1209600
|
206
213
|
end
|
214
|
+
|
215
|
+
def allow?(uri)
|
216
|
+
@options[:allow] && @options[:allow].include?(uri)
|
217
|
+
end
|
218
|
+
|
219
|
+
def bypass?(request)
|
220
|
+
root_request?(request) ||
|
221
|
+
allow?(request.path_info) ||
|
222
|
+
valid_auth_key?(request) ||
|
223
|
+
logged_in?(request)
|
224
|
+
end
|
207
225
|
end
|
208
226
|
end
|
@@ -6,24 +6,18 @@ module CloudKit
|
|
6
6
|
class OpenIDStore < OpenID::Store::Interface
|
7
7
|
@@store = nil
|
8
8
|
|
9
|
-
# Initialize an OpenIDStore
|
10
|
-
def initialize
|
9
|
+
# Initialize an OpenIDStore.
|
10
|
+
def initialize
|
11
11
|
unless @@store
|
12
|
-
association_view = ExtractionView.new(
|
13
|
-
:cloudkit_openid_server_handles,
|
14
|
-
:observe => :cloudkit_openid_associations,
|
15
|
-
:extract => [:server_url, :handle])
|
16
12
|
@@store = Store.new(
|
17
|
-
:collections => [:cloudkit_openid_associations, :cloudkit_openid_nonces]
|
18
|
-
:views => [association_view],
|
19
|
-
:adapter => SQLAdapter.new(uri))
|
13
|
+
:collections => [:cloudkit_openid_associations, :cloudkit_openid_nonces])
|
20
14
|
end
|
21
15
|
end
|
22
16
|
|
23
17
|
def get_association(server_url, handle=nil) #:nodoc:
|
24
18
|
options = {:server_url => server_url}
|
25
19
|
options.merge!(:handle => Base64.encode64(handle)) if (handle && handle != '')
|
26
|
-
result = @@store.get('/
|
20
|
+
result = @@store.get(CloudKit::URI.new('/cloudkit_openid_associations'), options)
|
27
21
|
return nil unless result.status == 200
|
28
22
|
return nil if result.parsed_content['total'] == 0
|
29
23
|
|
@@ -42,7 +36,7 @@ module CloudKit
|
|
42
36
|
|
43
37
|
def remove_association(server_url, handle) #:nodoc:
|
44
38
|
result = @@store.get(
|
45
|
-
'/
|
39
|
+
CloudKit::URI.new('/cloudkit_openid_associations'),
|
46
40
|
:server_url => server_url,
|
47
41
|
:handle => Base64.encode64(handle))
|
48
42
|
return nil unless result.status == 200
|
@@ -52,7 +46,7 @@ module CloudKit
|
|
52
46
|
|
53
47
|
uris = result.parsed_content['uris']
|
54
48
|
responses.each_with_index do |r, index|
|
55
|
-
@@store.delete(uris[index], :etag => r.etag)
|
49
|
+
@@store.delete(CloudKit::URI.new(uris[index]), :etag => r.etag)
|
56
50
|
end
|
57
51
|
end
|
58
52
|
|
@@ -65,18 +59,18 @@ module CloudKit
|
|
65
59
|
:issued => association.issued.to_i,
|
66
60
|
:lifetime => association.lifetime,
|
67
61
|
:assoc_type => association.assoc_type)
|
68
|
-
result = @@store.post('/cloudkit_openid_associations', :json => json)
|
62
|
+
result = @@store.post(CloudKit::URI.new('/cloudkit_openid_associations'), :json => json)
|
69
63
|
return (result.status == 201)
|
70
64
|
end
|
71
65
|
|
72
66
|
def use_nonce(server_url, timestamp, salt) #:nodoc:
|
73
67
|
return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
|
74
68
|
|
75
|
-
fragment = URI.escape(
|
69
|
+
fragment = ::URI.escape(
|
76
70
|
[server_url, timestamp, salt].join('-'),
|
77
|
-
Regexp.union(URI::REGEXP::UNSAFE, '/', ':'))
|
71
|
+
Regexp.union(::URI::REGEXP::UNSAFE, '/', ':'))
|
78
72
|
uri = "/cloudkit_openid_nonces/#{fragment}"
|
79
|
-
result = @@store.put(uri, :json => '{}')
|
73
|
+
result = @@store.put(CloudKit::URI.new(uri), :json => '{}')
|
80
74
|
return (result.status == 201)
|
81
75
|
end
|
82
76
|
|
@@ -92,10 +86,13 @@ module CloudKit
|
|
92
86
|
# TODO
|
93
87
|
end
|
94
88
|
|
89
|
+
# Return the version number for this store.
|
90
|
+
def version; 1; end
|
91
|
+
|
95
92
|
protected
|
96
93
|
|
97
94
|
def resolve_associations(parsed_content) #:nodoc:
|
98
|
-
uri_list = parsed_content['uris']
|
95
|
+
uri_list = parsed_content['uris'].map! { |u| CloudKit::URI.new(u) }
|
99
96
|
association_responses = @@store.resolve_uris(uri_list)
|
100
97
|
return association_responses, association_responses.map{|a| a.parsed_content}
|
101
98
|
end
|
data/lib/cloudkit/request.rb
CHANGED
@@ -20,6 +20,11 @@ module CloudKit
|
|
20
20
|
self.body.read
|
21
21
|
end
|
22
22
|
|
23
|
+
# Return a CloudKit::URI instance representing the rack request's path info.
|
24
|
+
def uri
|
25
|
+
@uri ||= CloudKit::URI.new(self.path_info)
|
26
|
+
end
|
27
|
+
|
23
28
|
# Return true if method, path, and required_params match.
|
24
29
|
def match?(method, path, required_params=[])
|
25
30
|
(request_method == method) &&
|
@@ -66,7 +71,7 @@ module CloudKit
|
|
66
71
|
|
67
72
|
# Unescape a value according to the OAuth spec.
|
68
73
|
def unescape(value)
|
69
|
-
URI.unescape(value.gsub('+', '%2B'))
|
74
|
+
::URI.unescape(value.gsub('+', '%2B'))
|
70
75
|
end
|
71
76
|
|
72
77
|
# Return the last path element in the request URI.
|
data/lib/cloudkit/service.rb
CHANGED
@@ -29,6 +29,8 @@ module CloudKit
|
|
29
29
|
|
30
30
|
@@lock = Mutex.new
|
31
31
|
|
32
|
+
attr_reader :store
|
33
|
+
|
32
34
|
def initialize(app, options)
|
33
35
|
@app = app
|
34
36
|
@collections = options[:collections]
|
@@ -36,9 +38,7 @@ module CloudKit
|
|
36
38
|
|
37
39
|
def call(env)
|
38
40
|
@@lock.synchronize do
|
39
|
-
@store = Store.new(
|
40
|
-
:adapter => SQLAdapter.new(env[CLOUDKIT_STORAGE_URI]),
|
41
|
-
:collections => @collections)
|
41
|
+
@store = Store.new(:collections => @collections)
|
42
42
|
end unless @store
|
43
43
|
|
44
44
|
request = Request.new(env)
|
@@ -55,7 +55,7 @@ module CloudKit
|
|
55
55
|
|
56
56
|
def get(request)
|
57
57
|
response = @store.get(
|
58
|
-
request.
|
58
|
+
request.uri,
|
59
59
|
{}.filter_merge!(
|
60
60
|
:remote_user => request.current_user,
|
61
61
|
:offset => request['offset'],
|
@@ -69,14 +69,14 @@ module CloudKit
|
|
69
69
|
return send(request['_method'].downcase)
|
70
70
|
end
|
71
71
|
@store.post(
|
72
|
-
request.
|
72
|
+
request.uri,
|
73
73
|
{:json => request.json}.filter_merge!(
|
74
74
|
:remote_user => request.current_user)).to_rack
|
75
75
|
end
|
76
76
|
|
77
77
|
def put(request)
|
78
78
|
@store.put(
|
79
|
-
request.
|
79
|
+
request.uri,
|
80
80
|
{:json => request.json}.filter_merge!(
|
81
81
|
:remote_user => request.current_user,
|
82
82
|
:etag => request.if_match)).to_rack
|
@@ -84,7 +84,7 @@ module CloudKit
|
|
84
84
|
|
85
85
|
def delete(request)
|
86
86
|
@store.delete(
|
87
|
-
request.
|
87
|
+
request.uri,
|
88
88
|
{}.filter_merge!(
|
89
89
|
:remote_user => request.current_user,
|
90
90
|
:etag => request.if_match)).to_rack
|
@@ -92,7 +92,7 @@ module CloudKit
|
|
92
92
|
|
93
93
|
def head(request)
|
94
94
|
response = @store.head(
|
95
|
-
request.
|
95
|
+
request.uri,
|
96
96
|
{}.filter_merge!(
|
97
97
|
:remote_user => request.current_user,
|
98
98
|
:offset => request['offset'],
|
@@ -102,15 +102,15 @@ module CloudKit
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def options(request)
|
105
|
-
@store.options(request.
|
105
|
+
@store.options(request.uri).to_rack
|
106
106
|
end
|
107
107
|
|
108
108
|
def inject_link_headers(request, response)
|
109
|
-
response['Link'] = versions_link_header(request) if
|
110
|
-
response['Link'] = resolved_link_header(request) if
|
111
|
-
response['Link'] = index_link_header(request) if
|
112
|
-
response['Link'] = resolved_link_header(request) if
|
113
|
-
response['Link'] = index_link_header(request) if
|
109
|
+
response['Link'] = versions_link_header(request) if request.uri.resource_uri?
|
110
|
+
response['Link'] = resolved_link_header(request) if request.uri.resource_collection_uri?
|
111
|
+
response['Link'] = index_link_header(request) if request.uri.resolved_resource_collection_uri?
|
112
|
+
response['Link'] = resolved_link_header(request) if request.uri.version_collection_uri?
|
113
|
+
response['Link'] = index_link_header(request) if request.uri.resolved_version_collection_uri?
|
114
114
|
end
|
115
115
|
|
116
116
|
def versions_link_header(request)
|
@@ -147,7 +147,7 @@ module CloudKit
|
|
147
147
|
|
148
148
|
def bypass?(request)
|
149
149
|
collection = @collections.detect{|type| request.path_info.match("/#{type.to_s}")}
|
150
|
-
!collection &&
|
150
|
+
!collection && !request.uri.meta_uri?
|
151
151
|
end
|
152
152
|
end
|
153
153
|
end
|
data/lib/cloudkit/store.rb
CHANGED
@@ -3,36 +3,22 @@ module CloudKit
|
|
3
3
|
# A functional storage interface with HTTP semantics and pluggable adapters.
|
4
4
|
class Store
|
5
5
|
include ResponseHelpers
|
6
|
+
include CloudKit::Util
|
6
7
|
|
7
|
-
# Initialize a new Store
|
8
|
-
#
|
8
|
+
# Initialize a new Store. All resources in a Store are automatically
|
9
|
+
# versioned.
|
9
10
|
#
|
10
11
|
# ===Options
|
11
|
-
# - :adapter - Optional. An instance of Adapter. Defaults to in-memory SQLite.
|
12
12
|
# - :collections - Array of resource collections to manage.
|
13
|
-
# - :views - Optional. Array of views to be updated based on JSON content.
|
14
13
|
#
|
15
14
|
# ===Example
|
16
15
|
# store = CloudKit::Store.new(:collections => [:foos, :bars])
|
17
16
|
#
|
18
|
-
#
|
19
|
-
# adapter = CloudKit::SQLAdapter.new('mysql://user:pass@localhost/my_db')
|
20
|
-
# fruit_color_view = CloudKit::ExtractionView.new(
|
21
|
-
# :fruits_by_color_and_season,
|
22
|
-
# :observe => :fruits,
|
23
|
-
# :extract => [:color, :season])
|
24
|
-
# store = CloudKit::Store.new(
|
25
|
-
# :adapter => adapter,
|
26
|
-
# :collections => [:foos, :fruits],
|
27
|
-
# :views => [fruit_color_view])
|
28
|
-
#
|
29
|
-
# See also: Adapter, ExtractionView, Response
|
17
|
+
# See also: Response
|
30
18
|
#
|
31
19
|
def initialize(options)
|
32
|
-
|
20
|
+
CloudKit.setup_storage_adapter unless CloudKit.storage_adapter
|
33
21
|
@collections = options[:collections]
|
34
|
-
@views = options[:views]
|
35
|
-
@views.each {|view| view.initialize_storage(@db)} if @views
|
36
22
|
end
|
37
23
|
|
38
24
|
# Retrieve a resource or collection of resources based on a URI.
|
@@ -45,7 +31,7 @@ module CloudKit
|
|
45
31
|
# - :remote_user - Optional. Scopes the dataset if provided.
|
46
32
|
# - :limit - Optional. Default is unlimited. Limit the number of records returned by a collection request.
|
47
33
|
# - :offset - Optional. Start the list of resources in a collection at offset (0-based).
|
48
|
-
# - :any - Optional. Not a literal ":any", but any key or keys
|
34
|
+
# - :any - Optional. Not a literal ":any", but any key or keys that are top level JSON keys. This is a starting point for future JSONPath/JSONQuery support.
|
49
35
|
#
|
50
36
|
# ===URI Types
|
51
37
|
# /cloudkit-meta
|
@@ -55,7 +41,6 @@ module CloudKit
|
|
55
41
|
# /{collection}/{uuid}/versions
|
56
42
|
# /{collection}/{uuid}/versions/_resolved
|
57
43
|
# /{collection}/{uuid}/versions/{etag}
|
58
|
-
# /{view}
|
59
44
|
#
|
60
45
|
# ===Examples
|
61
46
|
# get('/cloudkit-meta')
|
@@ -65,20 +50,18 @@ module CloudKit
|
|
65
50
|
# get('/foos/123')
|
66
51
|
# get('/foos/123/versions')
|
67
52
|
# get('/foos/123/versions/abc')
|
68
|
-
# get('/shiny_foos', :color => 'green')
|
69
53
|
#
|
70
54
|
# See also: {REST API}[http://getcloudkit.com/rest-api.html]
|
71
55
|
#
|
72
56
|
def get(uri, options={})
|
73
|
-
return invalid_entity_type if !valid_collection_type?(collection_type
|
74
|
-
return meta if meta_uri?
|
75
|
-
return resource_collection(uri, options) if resource_collection_uri?
|
76
|
-
return resolved_resource_collection(uri, options) if resolved_resource_collection_uri?
|
77
|
-
return resource(uri, options) if resource_uri?
|
78
|
-
return version_collection(uri, options) if version_collection_uri?
|
79
|
-
return resolved_version_collection(uri, options) if resolved_version_collection_uri?
|
80
|
-
return resource_version(uri, options) if resource_version_uri?
|
81
|
-
return view(uri, options) if view_uri?(uri)
|
57
|
+
return invalid_entity_type if !valid_collection_type?(uri.collection_type)
|
58
|
+
return meta if uri.meta_uri?
|
59
|
+
return resource_collection(uri, options) if uri.resource_collection_uri?
|
60
|
+
return resolved_resource_collection(uri, options) if uri.resolved_resource_collection_uri?
|
61
|
+
return resource(uri, options) if uri.resource_uri?
|
62
|
+
return version_collection(uri, options) if uri.version_collection_uri?
|
63
|
+
return resolved_version_collection(uri, options) if uri.resolved_version_collection_uri?
|
64
|
+
return resource_version(uri, options) if uri.resource_version_uri?
|
82
65
|
status_404
|
83
66
|
end
|
84
67
|
|
@@ -87,19 +70,14 @@ module CloudKit
|
|
87
70
|
# to the way CloudKit stores its ETags and Last-Modified information on
|
88
71
|
# write.
|
89
72
|
def head(uri, options={})
|
90
|
-
return invalid_entity_type unless @collections.include?(collection_type
|
91
|
-
if resource_uri?
|
73
|
+
return invalid_entity_type unless @collections.include?(uri.collection_type)
|
74
|
+
if uri.resource_uri? || uri.resource_version_uri?
|
92
75
|
# ETag and Last-Modified are already stored for single items, so a slight
|
93
76
|
# optimization can be made for HEAD requests.
|
94
|
-
result =
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
result = result.first
|
99
|
-
return status_410.head if result[:deleted]
|
100
|
-
return response(200, '', result[:etag], result[:last_modified])
|
101
|
-
end
|
102
|
-
status_404.head
|
77
|
+
result = CloudKit::Resource.first(options.merge(:uri => uri.string))
|
78
|
+
return status_404.head unless result
|
79
|
+
return status_410.head if result.deleted?
|
80
|
+
return response(200, '', result.etag, result.last_modified)
|
103
81
|
else
|
104
82
|
get(uri, options).head
|
105
83
|
end
|
@@ -110,7 +88,7 @@ module CloudKit
|
|
110
88
|
def put(uri, options={})
|
111
89
|
methods = methods_for_uri(uri)
|
112
90
|
return status_405(methods) unless methods.include?('PUT')
|
113
|
-
return invalid_entity_type unless @collections.include?(collection_type
|
91
|
+
return invalid_entity_type unless @collections.include?(uri.collection_type)
|
114
92
|
return data_required unless options[:json]
|
115
93
|
current_resource = resource(uri, options.excluding(:json, :etag, :remote_user))
|
116
94
|
return update_resource(uri, options) if current_resource.status == 200
|
@@ -122,9 +100,8 @@ module CloudKit
|
|
122
100
|
def post(uri, options={})
|
123
101
|
methods = methods_for_uri(uri)
|
124
102
|
return status_405(methods) unless methods.include?('POST')
|
125
|
-
return invalid_entity_type unless @collections.include?(collection_type
|
103
|
+
return invalid_entity_type unless @collections.include?(uri.collection_type)
|
126
104
|
return data_required unless options[:json]
|
127
|
-
uri = "#{collection_uri_fragment(uri)}/#{UUID.generate}"
|
128
105
|
create_resource(uri, options)
|
129
106
|
end
|
130
107
|
|
@@ -132,31 +109,15 @@ module CloudKit
|
|
132
109
|
def delete(uri, options={})
|
133
110
|
methods = methods_for_uri(uri)
|
134
111
|
return status_405(methods) unless methods.include?('DELETE')
|
135
|
-
return invalid_entity_type unless @collections.include?(collection_type
|
112
|
+
return invalid_entity_type unless @collections.include?(uri.collection_type)
|
136
113
|
return etag_required unless options[:etag]
|
137
|
-
|
138
|
-
|
139
|
-
if
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
version_uri = ''
|
145
|
-
@db.transaction do
|
146
|
-
version_uri = "#{item[:uri]}/versions/#{item[:etag]}"
|
147
|
-
original.update(:uri => version_uri)
|
148
|
-
@db[CLOUDKIT_STORE].insert(
|
149
|
-
:uri => item[:uri],
|
150
|
-
:collection_reference => item[:collection_reference],
|
151
|
-
:resource_reference => item[:resource_reference],
|
152
|
-
:remote_user => item[:remote_user],
|
153
|
-
:content => item[:content],
|
154
|
-
:deleted => true)
|
155
|
-
unmap(uri)
|
156
|
-
end
|
157
|
-
return json_meta_response(200, version_uri, item[:etag], item[:last_modified])
|
158
|
-
end
|
159
|
-
status_404
|
114
|
+
resource = CloudKit::Resource.first(options.excluding(:etag).merge(:uri => uri.string))
|
115
|
+
return status_404 unless (resource && (resource.remote_user == options[:remote_user]))
|
116
|
+
return status_410 if resource.deleted?
|
117
|
+
return status_412 if resource.etag != options[:etag]
|
118
|
+
|
119
|
+
resource.delete
|
120
|
+
return json_meta_response(200, resource.uri.string, resource.etag, resource.last_modified)
|
160
121
|
end
|
161
122
|
|
162
123
|
# Build a response containing the allowed methods for a given URI.
|
@@ -167,13 +128,13 @@ module CloudKit
|
|
167
128
|
|
168
129
|
# Return a list of allowed methods for a given URI.
|
169
130
|
def methods_for_uri(uri)
|
170
|
-
return meta_methods if meta_uri?
|
171
|
-
return resource_collection_methods if resource_collection_uri?
|
172
|
-
return resolved_resource_collection_methods if resolved_resource_collection_uri?
|
173
|
-
return resource_methods if resource_uri?
|
174
|
-
return version_collection_methods if version_collection_uri?
|
175
|
-
return resolved_version_collection_methods if resolved_version_collection_uri?
|
176
|
-
return resource_version_methods if resource_version_uri?
|
131
|
+
return meta_methods if uri.meta_uri?
|
132
|
+
return resource_collection_methods if uri.resource_collection_uri?
|
133
|
+
return resolved_resource_collection_methods if uri.resolved_resource_collection_uri?
|
134
|
+
return resource_methods if uri.resource_uri?
|
135
|
+
return version_collection_methods if uri.version_collection_uri?
|
136
|
+
return resolved_version_collection_methods if uri.resolved_version_collection_uri?
|
137
|
+
return resource_version_methods if uri.resource_version_uri?
|
177
138
|
end
|
178
139
|
|
179
140
|
# Return the list of methods allowed for the cloudkit-meta URI.
|
@@ -221,91 +182,17 @@ module CloudKit
|
|
221
182
|
['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS']
|
222
183
|
end
|
223
184
|
|
224
|
-
# Return the resource collection URI fragment.
|
225
|
-
# Example: collection_uri_fragment('/foos/123') => '/foos
|
226
|
-
def collection_uri_fragment(uri)
|
227
|
-
"/#{uri_components(uri)[0]}" rescue nil
|
228
|
-
end
|
229
|
-
|
230
|
-
# Return the resource collection referenced by a URI.
|
231
|
-
# Example: collection_type('/foos/123') => :foos
|
232
|
-
def collection_type(uri)
|
233
|
-
uri_components(uri)[0].to_sym rescue nil
|
234
|
-
end
|
235
|
-
|
236
|
-
# Return the URI for the current version of a resource.
|
237
|
-
# Example: current_resource_uri('/foos/123/versions/abc') => '/foos/123'
|
238
|
-
def current_resource_uri(uri)
|
239
|
-
"/#{uri_components(uri)[0..1].join('/')}" rescue nil
|
240
|
-
end
|
241
|
-
|
242
|
-
# Splits a URI into its components
|
243
|
-
def uri_components(uri)
|
244
|
-
uri.split('/').reject{|x| x == '' || x == nil} rescue []
|
245
|
-
end
|
246
|
-
|
247
|
-
# Returns true if URI matches /cloudkit-meta
|
248
|
-
def meta_uri?(uri)
|
249
|
-
c = uri_components(uri)
|
250
|
-
return c.size == 1 && c[0] == 'cloudkit-meta'
|
251
|
-
end
|
252
|
-
|
253
|
-
# Returns true if URI matches /{collection}
|
254
|
-
def resource_collection_uri?(uri)
|
255
|
-
c = uri_components(uri)
|
256
|
-
return c.size == 1 && @collections.include?(c[0].to_sym)
|
257
|
-
end
|
258
|
-
|
259
|
-
# Returns true if URI matches /{collection}/_resolved
|
260
|
-
def resolved_resource_collection_uri?(uri)
|
261
|
-
c = uri_components(uri)
|
262
|
-
return c.size == 2 && @collections.include?(c[0].to_sym) && c[1] == '_resolved'
|
263
|
-
end
|
264
|
-
|
265
|
-
# Returns true if URI matches /{collection}/{uuid}
|
266
|
-
def resource_uri?(uri)
|
267
|
-
c = uri_components(uri)
|
268
|
-
return c.size == 2 && @collections.include?(c[0].to_sym) && c[1] != '_resolved'
|
269
|
-
end
|
270
|
-
|
271
|
-
# Returns true if URI matches /{collection}/{uuid}/versions
|
272
|
-
def version_collection_uri?(uri)
|
273
|
-
c = uri_components(uri)
|
274
|
-
return c.size == 3 && @collections.include?(c[0].to_sym) && c[2] == 'versions'
|
275
|
-
end
|
276
|
-
|
277
|
-
# Returns true if URI matches /{collection}/{uuid}/versions/_resolved
|
278
|
-
def resolved_version_collection_uri?(uri)
|
279
|
-
c = uri_components(uri)
|
280
|
-
return c.size == 4 && @collections.include?(c[0].to_sym) && c[2] == 'versions' && c[3] == '_resolved'
|
281
|
-
end
|
282
|
-
|
283
|
-
# Returns true if URI matches /{collection}/{uuid}/versions/{etag}
|
284
|
-
def resource_version_uri?(uri)
|
285
|
-
c = uri_components(uri)
|
286
|
-
return c.size == 4 && @collections.include?(c[0].to_sym) && c[2] == 'versions' && c[3] != '_resolved'
|
287
|
-
end
|
288
|
-
|
289
|
-
# Returns true if URI matches /{view}
|
290
|
-
def view_uri?(uri)
|
291
|
-
c = uri_components(uri)
|
292
|
-
return c.size == 1 && @views && @views.map{|v| v.name}.include?(c[0].to_sym)
|
293
|
-
end
|
294
|
-
|
295
185
|
# Return an array containing the response for each URI in a list.
|
296
|
-
def resolve_uris(uris)
|
186
|
+
def resolve_uris(uris) # TODO - remove if no longer needed
|
297
187
|
result = []
|
298
188
|
uris.each do |uri|
|
299
189
|
result << get(uri)
|
300
190
|
end
|
301
191
|
result
|
302
192
|
end
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
@db.schema.keys.each do |table|
|
307
|
-
@db[table.gsub('`','').to_sym].delete
|
308
|
-
end
|
193
|
+
|
194
|
+
def storage_adapter
|
195
|
+
CloudKit.storage_adapter
|
309
196
|
end
|
310
197
|
|
311
198
|
# Return the version number of this Store.
|
@@ -322,145 +209,96 @@ module CloudKit
|
|
322
209
|
# Return a list of resource URIs for the given collection URI. Sorted by
|
323
210
|
# Last-Modified date in descending order.
|
324
211
|
def resource_collection(uri, options)
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
bundle_collection_result(uri, options, result)
|
212
|
+
filter = options.excluding(:offset, :limit).merge(
|
213
|
+
:deleted => false,
|
214
|
+
:collection_reference => uri.collection_uri_fragment,
|
215
|
+
:archived => false)
|
216
|
+
result = CloudKit::Resource.current(filter)
|
217
|
+
bundle_collection_result(uri.string, options, result)
|
332
218
|
end
|
333
219
|
|
334
220
|
# Return all documents and their associated metadata for the given
|
335
221
|
# collection URI.
|
336
222
|
def resolved_resource_collection(uri, options)
|
337
|
-
result =
|
338
|
-
|
339
|
-
|
340
|
-
filter('resource_reference = uri').
|
341
|
-
reverse_order(:id)
|
223
|
+
result = CloudKit::Resource.current(
|
224
|
+
options.excluding(:offset, :limit).merge(
|
225
|
+
:collection_reference => uri.collection_uri_fragment))
|
342
226
|
bundle_resolved_collection_result(uri, options, result)
|
343
227
|
end
|
344
228
|
|
345
229
|
# Return the resource for the given URI. Return 404 if not found or if
|
346
230
|
# protected and unauthorized, 410 if authorized but deleted.
|
347
231
|
def resource(uri, options)
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
if result.any?
|
352
|
-
result = result.first
|
353
|
-
return status_410 if result[:deleted]
|
354
|
-
return response(200, result[:content], result[:etag], result[:last_modified])
|
232
|
+
if resource = CloudKit::Resource.first(options.merge!(:uri => uri.string))
|
233
|
+
return status_410 if resource.deleted?
|
234
|
+
return response(200, resource.json, resource.etag, resource.last_modified)
|
355
235
|
end
|
356
236
|
status_404
|
357
237
|
end
|
358
238
|
|
359
239
|
# Return a collection of URIs for all versions of a resource including the
|
360
|
-
#current version. Sorted by Last-Modified date in descending order.
|
240
|
+
# current version. Sorted by Last-Modified date in descending order.
|
361
241
|
def version_collection(uri, options)
|
362
|
-
found =
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
reverse_order(:id)
|
372
|
-
bundle_collection_result(uri, options, result)
|
242
|
+
found = CloudKit::Resource.first(
|
243
|
+
options.excluding(:offset, :limit).merge(
|
244
|
+
:uri => uri.current_resource_uri))
|
245
|
+
return status_404 unless found
|
246
|
+
result = CloudKit::Resource.all( # TODO - just use found.versions
|
247
|
+
options.excluding(:offset, :limit).merge(
|
248
|
+
:resource_reference => uri.current_resource_uri,
|
249
|
+
:deleted => false))
|
250
|
+
bundle_collection_result(uri.string, options, result)
|
373
251
|
end
|
374
252
|
|
375
253
|
# Return all document versions and their associated metadata for a given
|
376
254
|
# resource including the current version. Sorted by Last-Modified date in
|
377
255
|
# descending order.
|
378
256
|
def resolved_version_collection(uri, options)
|
379
|
-
found =
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
reverse_order(:id)
|
257
|
+
found = CloudKit::Resource.first(
|
258
|
+
options.excluding(:offset, :limit).merge(
|
259
|
+
:uri => uri.current_resource_uri))
|
260
|
+
return status_404 unless found
|
261
|
+
result = CloudKit::Resource.all(
|
262
|
+
options.excluding(:offset, :limit).merge(
|
263
|
+
:resource_reference => uri.current_resource_uri,
|
264
|
+
:deleted => false))
|
388
265
|
bundle_resolved_collection_result(uri, options, result)
|
389
266
|
end
|
390
267
|
|
391
268
|
# Return a specific version of a resource.
|
392
269
|
def resource_version(uri, options)
|
393
|
-
result =
|
394
|
-
|
395
|
-
|
396
|
-
return status_404 unless result.any?
|
397
|
-
result = result.first
|
398
|
-
response(200, result[:content], result[:etag], result[:last_modified])
|
399
|
-
end
|
400
|
-
|
401
|
-
# Return a list of URIs for all resources matching the list of key value
|
402
|
-
# pairs provided in the options arg.
|
403
|
-
def view(uri, options)
|
404
|
-
result = @db[collection_type(uri)].
|
405
|
-
select(:uri).
|
406
|
-
filter(options.excluding(:offset, :limit))
|
407
|
-
bundle_collection_result(uri, options, result)
|
270
|
+
result = CloudKit::Resource.first(options.merge(:uri => uri.string))
|
271
|
+
return status_404 unless result
|
272
|
+
response(200, result.json, result.etag, result.last_modified)
|
408
273
|
end
|
409
274
|
|
410
275
|
# Create a resource at the specified URI.
|
411
276
|
def create_resource(uri, options)
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
@db[CLOUDKIT_STORE].insert(
|
416
|
-
:uri => uri,
|
417
|
-
:collection_reference => collection_uri_fragment(uri),
|
418
|
-
:resource_reference => uri,
|
419
|
-
:etag => etag,
|
420
|
-
:last_modified => last_modified,
|
421
|
-
:remote_user => options[:remote_user],
|
422
|
-
:content => options[:json])
|
423
|
-
map(uri, data)
|
424
|
-
json_meta_response(201, uri, etag, last_modified)
|
277
|
+
JSON.parse(options[:json]) rescue (return status_422)
|
278
|
+
resource = CloudKit::Resource.create(uri, options[:json], options[:remote_user])
|
279
|
+
json_meta_response(201, resource.uri.string, resource.etag, resource.last_modified)
|
425
280
|
end
|
426
281
|
|
427
282
|
# Update the resource at the specified URI. Requires the :etag option.
|
428
283
|
def update_resource(uri, options)
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
etag = UUID.generate
|
438
|
-
last_modified = timestamp
|
439
|
-
@db.transaction do
|
440
|
-
original.update(:uri => "#{uri}/versions/#{item[:etag]}")
|
441
|
-
@db[CLOUDKIT_STORE].insert(
|
442
|
-
:uri => uri,
|
443
|
-
:collection_reference => item[:collection_reference],
|
444
|
-
:resource_reference => item[:resource_reference],
|
445
|
-
:etag => etag,
|
446
|
-
:last_modified => last_modified,
|
447
|
-
:remote_user => options[:remote_user],
|
448
|
-
:content => options[:json])
|
449
|
-
end
|
450
|
-
map(uri, data)
|
451
|
-
return json_meta_response(200, uri, etag, last_modified)
|
452
|
-
end
|
453
|
-
status_404
|
284
|
+
JSON.parse(options[:json]) rescue (return status_422)
|
285
|
+
resource = CloudKit::Resource.first(
|
286
|
+
options.excluding(:json, :etag).merge(:uri => uri.string))
|
287
|
+
return status_404 unless (resource && (resource.remote_user == options[:remote_user]))
|
288
|
+
return etag_required unless options[:etag]
|
289
|
+
return status_412 unless options[:etag] == resource.etag
|
290
|
+
resource.update(options[:json])
|
291
|
+
return json_meta_response(200, uri.string, resource.etag, resource.last_modified)
|
454
292
|
end
|
455
293
|
|
456
294
|
# Bundle a collection of results as a list of URIs for the response.
|
457
295
|
def bundle_collection_result(uri, options, result)
|
458
|
-
total = result.
|
296
|
+
total = result.size
|
459
297
|
offset = options[:offset].try(:to_i) || 0
|
460
298
|
max = options[:limit] ? offset + options[:limit].to_i : total
|
461
|
-
list = result.
|
299
|
+
list = result.to_a[offset...max].map{|r| r.uri}
|
462
300
|
json = uri_list(list, total, offset)
|
463
|
-
last_modified = result.first
|
301
|
+
last_modified = result.first.try(:last_modified) if result.any?
|
464
302
|
response(200, json, build_etag(json), last_modified)
|
465
303
|
end
|
466
304
|
|
@@ -468,18 +306,18 @@ module CloudKit
|
|
468
306
|
# metadata (last_modified, uri, etag) that would have accompanied a response
|
469
307
|
# to their singular request.
|
470
308
|
def bundle_resolved_collection_result(uri, options, result)
|
471
|
-
total = result.
|
309
|
+
total = result.size
|
472
310
|
offset = options[:offset].try(:to_i) || 0
|
473
311
|
max = options[:limit] ? offset + options[:limit].to_i : total
|
474
|
-
list = result.
|
312
|
+
list = result.to_a[offset...max]
|
475
313
|
json = resource_list(list, total, offset)
|
476
|
-
last_modified = result.first
|
314
|
+
last_modified = result.first.last_modified if result.any?
|
477
315
|
response(200, json, build_etag(json), last_modified)
|
478
316
|
end
|
479
317
|
|
480
318
|
# Generate a JSON URI list.
|
481
319
|
def uri_list(list, total, offset)
|
482
|
-
JSON.generate(:total => total, :offset => offset, :uris => list)
|
320
|
+
JSON.generate(:total => total, :offset => offset, :uris => list.map { |u| u.string })
|
483
321
|
end
|
484
322
|
|
485
323
|
# Generate a JSON document list.
|
@@ -487,10 +325,10 @@ module CloudKit
|
|
487
325
|
results = []
|
488
326
|
list.each do |resource|
|
489
327
|
results << {
|
490
|
-
:uri => resource
|
491
|
-
:etag => resource
|
492
|
-
:last_modified => resource
|
493
|
-
:document => resource
|
328
|
+
:uri => resource.uri.string,
|
329
|
+
:etag => resource.etag,
|
330
|
+
:last_modified => resource.last_modified,
|
331
|
+
:document => resource.json}
|
494
332
|
end
|
495
333
|
JSON.generate(:total => total, :offset => offset, :documents => results)
|
496
334
|
end
|
@@ -502,34 +340,9 @@ module CloudKit
|
|
502
340
|
MD5::md5(data.to_s).hexdigest
|
503
341
|
end
|
504
342
|
|
505
|
-
# Returns true if the collection type represents a view.
|
506
|
-
def is_view?(collection_type)
|
507
|
-
@views && @views.map{|v| v.name}.include?(collection_type)
|
508
|
-
end
|
509
|
-
|
510
343
|
# Returns true if the collection type is valid for this Store.
|
511
344
|
def valid_collection_type?(collection_type)
|
512
|
-
@collections.include?(collection_type) ||
|
513
|
-
is_view?(collection_type) ||
|
514
|
-
collection_type.to_s == 'cloudkit-meta'
|
515
|
-
end
|
516
|
-
|
517
|
-
# Delegates the mapping of data from a resource into a view.
|
518
|
-
def map(uri, data)
|
519
|
-
@views.each{|view| view.map(@db, collection_type(uri), uri, data)} if @views
|
345
|
+
@collections.include?(collection_type) || collection_type.to_s == 'cloudkit-meta'
|
520
346
|
end
|
521
|
-
|
522
|
-
# Delegates removal of view data.
|
523
|
-
def unmap(uri)
|
524
|
-
@views.each{|view| view.unmap(@db, collection_type(uri), uri)} if @views
|
525
|
-
end
|
526
|
-
|
527
|
-
# Return a HTTP date representing 'now.'
|
528
|
-
def timestamp
|
529
|
-
Time.now.httpdate
|
530
|
-
end
|
531
|
-
|
532
|
-
# Return the adapter instance used by this Store.
|
533
|
-
def db; @db; end
|
534
347
|
end
|
535
348
|
end
|