better-riak-client 1.0.5
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/LICENSE +16 -0
- data/README.markdown +198 -0
- data/RELEASE_NOTES.md +211 -0
- data/better-riak-client.gemspec +61 -0
- data/erl_src/riak_kv_test014_backend.beam +0 -0
- data/erl_src/riak_kv_test014_backend.erl +189 -0
- data/erl_src/riak_kv_test_backend.beam +0 -0
- data/erl_src/riak_kv_test_backend.erl +697 -0
- data/erl_src/riak_search_test_backend.beam +0 -0
- data/erl_src/riak_search_test_backend.erl +175 -0
- data/lib/riak/bucket.rb +221 -0
- data/lib/riak/client/beefcake/messages.rb +213 -0
- data/lib/riak/client/beefcake/object_methods.rb +111 -0
- data/lib/riak/client/beefcake_protobuffs_backend.rb +226 -0
- data/lib/riak/client/decaying.rb +36 -0
- data/lib/riak/client/excon_backend.rb +162 -0
- data/lib/riak/client/feature_detection.rb +88 -0
- data/lib/riak/client/http_backend/configuration.rb +211 -0
- data/lib/riak/client/http_backend/key_streamer.rb +43 -0
- data/lib/riak/client/http_backend/object_methods.rb +106 -0
- data/lib/riak/client/http_backend/request_headers.rb +34 -0
- data/lib/riak/client/http_backend/transport_methods.rb +201 -0
- data/lib/riak/client/http_backend.rb +340 -0
- data/lib/riak/client/net_http_backend.rb +82 -0
- data/lib/riak/client/node.rb +115 -0
- data/lib/riak/client/protobuffs_backend.rb +173 -0
- data/lib/riak/client/search.rb +91 -0
- data/lib/riak/client.rb +540 -0
- data/lib/riak/cluster.rb +151 -0
- data/lib/riak/core_ext/blank.rb +53 -0
- data/lib/riak/core_ext/deep_dup.rb +13 -0
- data/lib/riak/core_ext/extract_options.rb +7 -0
- data/lib/riak/core_ext/json.rb +15 -0
- data/lib/riak/core_ext/slice.rb +18 -0
- data/lib/riak/core_ext/stringify_keys.rb +10 -0
- data/lib/riak/core_ext/symbolize_keys.rb +10 -0
- data/lib/riak/core_ext/to_param.rb +31 -0
- data/lib/riak/core_ext.rb +7 -0
- data/lib/riak/encoding.rb +6 -0
- data/lib/riak/failed_request.rb +81 -0
- data/lib/riak/i18n.rb +5 -0
- data/lib/riak/json.rb +52 -0
- data/lib/riak/link.rb +94 -0
- data/lib/riak/locale/en.yml +53 -0
- data/lib/riak/locale/fr.yml +52 -0
- data/lib/riak/map_reduce/filter_builder.rb +103 -0
- data/lib/riak/map_reduce/phase.rb +98 -0
- data/lib/riak/map_reduce.rb +225 -0
- data/lib/riak/map_reduce_error.rb +7 -0
- data/lib/riak/node/configuration.rb +293 -0
- data/lib/riak/node/console.rb +133 -0
- data/lib/riak/node/control.rb +207 -0
- data/lib/riak/node/defaults.rb +83 -0
- data/lib/riak/node/generation.rb +106 -0
- data/lib/riak/node/log.rb +34 -0
- data/lib/riak/node/version.rb +43 -0
- data/lib/riak/node.rb +38 -0
- data/lib/riak/robject.rb +318 -0
- data/lib/riak/search.rb +3 -0
- data/lib/riak/serializers.rb +74 -0
- data/lib/riak/stamp.rb +77 -0
- data/lib/riak/test_server.rb +89 -0
- data/lib/riak/util/escape.rb +76 -0
- data/lib/riak/util/headers.rb +53 -0
- data/lib/riak/util/multipart/stream_parser.rb +62 -0
- data/lib/riak/util/multipart.rb +52 -0
- data/lib/riak/util/tcp_socket_extensions.rb +58 -0
- data/lib/riak/util/translation.rb +19 -0
- data/lib/riak/version.rb +3 -0
- data/lib/riak/walk_spec.rb +105 -0
- data/lib/riak.rb +21 -0
- metadata +348 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
module Riak
|
2
|
+
class Client
|
3
|
+
# Methods that can be used to determine whether certain features
|
4
|
+
# are supported by the Riak node to which the client backend is
|
5
|
+
# connected.
|
6
|
+
#
|
7
|
+
# Backends must implement the "get_server_version" method,
|
8
|
+
# returning a string representing the Riak node's version. This is
|
9
|
+
# implemented on HTTP using the stats resource, and on Protocol
|
10
|
+
# Buffers using the RpbGetServerInfoReq message.
|
11
|
+
module FeatureDetection
|
12
|
+
# Constants representing Riak versions
|
13
|
+
VERSION = {
|
14
|
+
1 => Gem::Version.new("1.0.0"),
|
15
|
+
1.1 => Gem::Version.new("1.1.0"),
|
16
|
+
1.2 => Gem::Version.new("1.2.0")
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
# @return [String] the version of the Riak node
|
20
|
+
# @abstract
|
21
|
+
def get_server_version
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Gem::Version] the version of the Riak node to which
|
26
|
+
# this backend is connected
|
27
|
+
def server_version
|
28
|
+
@server_version ||= Gem::Version.new(get_server_version)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [true,false] whether MapReduce requests can be submitted without
|
32
|
+
# phases.
|
33
|
+
def mapred_phaseless?
|
34
|
+
at_least? VERSION[1.1]
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [true,false] whether secondary index queries are
|
38
|
+
# supported over Protocol Buffers
|
39
|
+
def pb_indexes?
|
40
|
+
at_least? VERSION[1.2]
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [true,false] whether search queries are supported over
|
44
|
+
# Protocol Buffers
|
45
|
+
def pb_search?
|
46
|
+
at_least? VERSION[1.2]
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [true,false] whether conditional fetch/store semantics
|
50
|
+
# are supported over Protocol Buffers
|
51
|
+
def pb_conditionals?
|
52
|
+
at_least? VERSION[1]
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [true,false] whether additional quorums and FSM
|
56
|
+
# controls are available, e.g. primary quorums, basic_quorum,
|
57
|
+
# notfound_ok
|
58
|
+
def quorum_controls?
|
59
|
+
at_least? VERSION[1]
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [true,false] whether "not found" responses might
|
63
|
+
# include vclocks
|
64
|
+
def tombstone_vclocks?
|
65
|
+
at_least? VERSION[1]
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [true,false] whether partial-fetches (vclock and
|
69
|
+
# metadata only) are supported over Protocol Buffers
|
70
|
+
def pb_head?
|
71
|
+
at_least? VERSION[1]
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
# @return [true,false] whether the server version is the same or
|
76
|
+
# newer than the requested version
|
77
|
+
def at_least?(version)
|
78
|
+
server_version >= version
|
79
|
+
end
|
80
|
+
|
81
|
+
# Backends should call this when their connection is interrupted
|
82
|
+
# or reset so as to facilitate rolling upgrades
|
83
|
+
def reset_server_version
|
84
|
+
@server_version = nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
|
2
|
+
require 'riak/failed_request'
|
3
|
+
require 'riak/client/http_backend'
|
4
|
+
require 'riak/link'
|
5
|
+
|
6
|
+
module Riak
|
7
|
+
class Client
|
8
|
+
class HTTPBackend
|
9
|
+
# Riak 0.14 provides a root URL that enumerates all of the
|
10
|
+
# HTTP endpoints and their paths. This module adds methods to
|
11
|
+
# auto-discover those endpoints via the root URL. It also adds
|
12
|
+
# methods for generating URL paths for specific resources.
|
13
|
+
module Configuration
|
14
|
+
# @return [URI] a URL path to the "ping" resource
|
15
|
+
def ping_path
|
16
|
+
path(riak_kv_wm_ping)
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [URI] a URL path to the "stats" resource
|
20
|
+
def stats_path
|
21
|
+
path(riak_kv_wm_stats)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [URI] a URL path to the "mapred" resource
|
25
|
+
# @param options [Hash] query parameters, e.g. chunked=true
|
26
|
+
def mapred_path(options={})
|
27
|
+
path(riak_kv_wm_mapred, options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [URI] a URL path for the "buckets list" resource
|
31
|
+
def bucket_list_path(options={})
|
32
|
+
if new_scheme?
|
33
|
+
path(riak_kv_wm_buckets, options.merge(:buckets => true))
|
34
|
+
else
|
35
|
+
path(riak_kv_wm_raw, options.merge(:buckets => true))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [URI] a URL path for the "bucket properties"
|
40
|
+
# resource
|
41
|
+
# @param [String] bucket the bucket name
|
42
|
+
def bucket_properties_path(bucket, options={})
|
43
|
+
if new_scheme?
|
44
|
+
path(riak_kv_wm_buckets, escape(bucket), "props", options)
|
45
|
+
else
|
46
|
+
path(riak_kv_wm_raw, escape(bucket), options.merge(:props => true, :keys => false))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [URI] a URL path for the "list keys" resource
|
51
|
+
# @param [String] bucket the bucket whose keys to list
|
52
|
+
# @param [Hash] options query parameters, e.g. keys=stream
|
53
|
+
def key_list_path(bucket, options={:keys => true})
|
54
|
+
if new_scheme?
|
55
|
+
path(riak_kv_wm_buckets, escape(bucket), "keys", options)
|
56
|
+
else
|
57
|
+
path(riak_kv_wm_raw, escape(bucket), options.merge(:props => false))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [URI] a URL path for the "object" resource
|
62
|
+
# @param [String] bucket the bucket of the object
|
63
|
+
# @param [String,nil] key the key of the object, or nil for a
|
64
|
+
# server-assigned key when using POST
|
65
|
+
def object_path(bucket, key, options={})
|
66
|
+
key = escape(key) if key
|
67
|
+
if new_scheme?
|
68
|
+
path([riak_kv_wm_buckets, escape(bucket), "keys", key, options].compact)
|
69
|
+
else
|
70
|
+
path([riak_kv_wm_raw, escape(bucket), key, options].compact)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [URI] a URL path for the "link-walking" resource
|
75
|
+
# @param [String] bucket the bucket of the origin object
|
76
|
+
# @param [String] key the key of the origin object
|
77
|
+
# @param [Array<WalkSpec>] specs a list of walk specifications
|
78
|
+
# to traverse
|
79
|
+
def link_walk_path(bucket, key, specs)
|
80
|
+
specs = specs.map {|s| s.to_s }
|
81
|
+
if new_scheme?
|
82
|
+
path(riak_kv_wm_buckets, escape(bucket), "keys", escape(key), *specs)
|
83
|
+
else
|
84
|
+
path(riak_kv_wm_link_walker, escape(bucket), escape(key), *specs)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [URI] a URL path for the "index range query"
|
89
|
+
# resource
|
90
|
+
# @param [String] bucket the bucket whose index to query
|
91
|
+
# @param [String] index the name of the index to query
|
92
|
+
# @param [String,Integer] start the start of the range
|
93
|
+
# @param [String,Integer] finish the end of the range
|
94
|
+
def index_range_path(bucket, index, start, finish, options={})
|
95
|
+
raise t('indexes_unsupported') unless new_scheme?
|
96
|
+
path(riak_kv_wm_buckets, escape(bucket), "index", escape(index), escape(start.to_s), escape(finish.to_s), options)
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [URI] a URL path for the "index range query"
|
100
|
+
# resource
|
101
|
+
# @param [String] bucket the bucket whose index to query
|
102
|
+
# @param [String] index the name of the index to query
|
103
|
+
# @param [String,Integer] start the start of the range
|
104
|
+
# @param [String,Integer] finish the end of the range
|
105
|
+
def index_eq_path(bucket, index, value, options={})
|
106
|
+
raise t('indexes_unsupported') unless new_scheme?
|
107
|
+
path(riak_kv_wm_buckets, escape(bucket), "index", escape(index), escape(value.to_s), options)
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return [URI] a URL path for a Solr query resource
|
111
|
+
# @param [String] index the index to query
|
112
|
+
# @param [String] query the Lucene-style query string
|
113
|
+
# @param [Hash] options additional query options
|
114
|
+
def solr_select_path(index, query, options={})
|
115
|
+
raise t('search_unsupported') unless riak_solr_searcher_wm
|
116
|
+
options = {"q" => query, "wt" => "json"}.merge(options)
|
117
|
+
if index
|
118
|
+
path(riak_solr_searcher_wm, index, 'select', options)
|
119
|
+
else
|
120
|
+
path(riak_solr_searcher_wm, 'select', options)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return [URI] a URL path for a Solr update resource
|
125
|
+
# @param [String] index the index to update
|
126
|
+
def solr_update_path(index)
|
127
|
+
raise t('search_unsupported') unless riak_solr_indexer_wm
|
128
|
+
if index
|
129
|
+
path(riak_solr_indexer_wm, index, 'update')
|
130
|
+
else
|
131
|
+
path(riak_solr_indexer_wm, 'update')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# @return [URI] a URL path for the Luwak interface
|
136
|
+
def luwak_path(key)
|
137
|
+
raise t('luwak_unsupported') unless luwak_wm_file
|
138
|
+
if key
|
139
|
+
path(luwak_wm_file, escape(key))
|
140
|
+
else
|
141
|
+
path(luwak_wm_file)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
def server_config
|
147
|
+
@server_config ||= {}.tap do |hash|
|
148
|
+
begin
|
149
|
+
response = get(200, path("/"))
|
150
|
+
Link.parse(response[:headers]['link'].first).each {|l| hash[l.tag.intern] ||= l.url }
|
151
|
+
rescue Riak::FailedRequest
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def new_scheme?
|
157
|
+
!riak_kv_wm_buckets.nil?
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_server_version
|
161
|
+
begin
|
162
|
+
# Attempt to grab the server version from the stats resource
|
163
|
+
stats = send(:stats)
|
164
|
+
stats['riak_kv_version']
|
165
|
+
rescue FailedRequest
|
166
|
+
# If stats is disabled, we can't assume the Riak version
|
167
|
+
# is >= 1.1. However, we can assume the new URL scheme is
|
168
|
+
# at least version 1.0.
|
169
|
+
new_scheme? ? "1.0.0" : "0.14.0"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def riak_kv_wm_buckets
|
174
|
+
server_config[:riak_kv_wm_buckets]
|
175
|
+
end
|
176
|
+
|
177
|
+
def riak_kv_wm_raw
|
178
|
+
server_config[:riak_kv_wm_raw] || node.http_paths[:prefix]
|
179
|
+
end
|
180
|
+
|
181
|
+
def riak_kv_wm_link_walker
|
182
|
+
server_config[:riak_kv_wm_link_walker] || node.http_paths[:prefix]
|
183
|
+
end
|
184
|
+
|
185
|
+
def riak_kv_wm_mapred
|
186
|
+
server_config[:riak_kv_wm_mapred] || node.http_paths[:mapred]
|
187
|
+
end
|
188
|
+
|
189
|
+
def riak_kv_wm_ping
|
190
|
+
server_config[:riak_kv_wm_ping] || "/ping"
|
191
|
+
end
|
192
|
+
|
193
|
+
def riak_kv_wm_stats
|
194
|
+
server_config[:riak_kv_wm_stats] || "/stats"
|
195
|
+
end
|
196
|
+
|
197
|
+
def riak_solr_searcher_wm
|
198
|
+
server_config[:riak_solr_searcher_wm]
|
199
|
+
end
|
200
|
+
|
201
|
+
def riak_solr_indexer_wm
|
202
|
+
server_config[:riak_solr_indexer_wm]
|
203
|
+
end
|
204
|
+
|
205
|
+
def luwak_wm_file
|
206
|
+
server_config[:luwak_wm_file]
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'riak/util/escape'
|
2
|
+
require 'riak/json'
|
3
|
+
|
4
|
+
module Riak
|
5
|
+
class Client
|
6
|
+
class HTTPBackend
|
7
|
+
# @private
|
8
|
+
class KeyStreamer
|
9
|
+
include Util::Escape
|
10
|
+
|
11
|
+
def initialize(block)
|
12
|
+
@buffer = ""
|
13
|
+
@block = block
|
14
|
+
end
|
15
|
+
|
16
|
+
def accept(chunk)
|
17
|
+
@buffer << chunk
|
18
|
+
consume
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_proc
|
22
|
+
method(:accept).to_proc
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def consume
|
27
|
+
while @buffer =~ /\}\{/
|
28
|
+
stream($~.pre_match + '}')
|
29
|
+
@buffer = '{' + $~.post_match
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def stream(str)
|
34
|
+
obj = JSON.parse(str) rescue nil
|
35
|
+
if obj && obj['keys']
|
36
|
+
@block.call obj['keys'].map(&method(:maybe_unescape))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'set'
|
3
|
+
require 'time'
|
4
|
+
require 'riak/client/http_backend'
|
5
|
+
require 'riak/robject'
|
6
|
+
require 'riak/link'
|
7
|
+
require 'riak/util/multipart'
|
8
|
+
|
9
|
+
module Riak
|
10
|
+
class Client
|
11
|
+
class HTTPBackend
|
12
|
+
# Methods for assisting with the handling of {RObject}s present
|
13
|
+
# in HTTP requests and responses.
|
14
|
+
module ObjectMethods
|
15
|
+
# HTTP header hash that will be sent along when reloading the object
|
16
|
+
# @return [Hash] hash of HTTP headers
|
17
|
+
def reload_headers(robject)
|
18
|
+
{}.tap do |h|
|
19
|
+
h['If-None-Match'] = robject.etag if robject.etag.present?
|
20
|
+
h['If-Modified-Since'] = robject.last_modified.httpdate if robject.last_modified.present?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# HTTP header hash that will be sent along when storing the object
|
25
|
+
# @return [Hash] hash of HTTP Headers
|
26
|
+
def store_headers(robject)
|
27
|
+
{}.tap do |hash|
|
28
|
+
hash["Content-Type"] = robject.content_type
|
29
|
+
hash["X-Riak-Vclock"] = robject.vclock if robject.vclock
|
30
|
+
if robject.prevent_stale_writes && robject.etag.present?
|
31
|
+
hash["If-Match"] = robject.etag
|
32
|
+
elsif robject.prevent_stale_writes
|
33
|
+
hash["If-None-Match"] = "*"
|
34
|
+
end
|
35
|
+
unless robject.links.blank?
|
36
|
+
hash["Link"] = robject.links.reject {|l| l.rel == "up" }.map {|l| l.to_s(new_scheme?) }.join(", ")
|
37
|
+
end
|
38
|
+
unless robject.meta.blank?
|
39
|
+
robject.meta.each do |k,v|
|
40
|
+
hash["X-Riak-Meta-#{k}"] = v.to_s
|
41
|
+
end
|
42
|
+
end
|
43
|
+
unless robject.indexes.blank?
|
44
|
+
robject.indexes.each do |k,v|
|
45
|
+
hash["X-Riak-Index-#{k}"] = v.to_a.sort.map {|i| i.to_s }.join(", ")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Load object data from an HTTP response
|
52
|
+
# @param [Hash] response a response from {Riak::Client::HTTPBackend}
|
53
|
+
def load_object(robject, response)
|
54
|
+
extract_header(robject, response, "location", :key) {|v| URI.unescape(v.match(%r{.*/(.*?)(\?.*)?$})[1]) }
|
55
|
+
extract_header(robject, response, "content-type", :content_type)
|
56
|
+
extract_header(robject, response, "x-riak-vclock", :vclock)
|
57
|
+
extract_header(robject, response, "link", :links) {|v| Set.new(Link.parse(v)) }
|
58
|
+
extract_header(robject, response, "etag", :etag)
|
59
|
+
extract_header(robject, response, "last-modified", :last_modified) {|v| Time.httpdate(v) }
|
60
|
+
robject.meta = response[:headers].inject({}) do |h,(k,v)|
|
61
|
+
if k =~ /x-riak-meta-(.*)/i
|
62
|
+
h[$1] = v
|
63
|
+
end
|
64
|
+
h
|
65
|
+
end
|
66
|
+
robject.indexes = response[:headers].inject(Hash.new {|h,k| h[k] = Set.new }) do |h,(k,v)|
|
67
|
+
if k =~ /x-riak-index-((?:.*)_(?:int|bin))$/i
|
68
|
+
key = $1
|
69
|
+
h[key].merge Array(v).map {|vals| vals.split(/,\s*/).map {|i| key =~ /int$/ ? i.to_i : i } }.flatten
|
70
|
+
end
|
71
|
+
h
|
72
|
+
end
|
73
|
+
robject.conflict = (response[:code] && response[:code].to_i == 300 && robject.content_type =~ /multipart\/mixed/)
|
74
|
+
robject.siblings = robject.conflict? ? extract_siblings(robject, response[:body]) : nil
|
75
|
+
robject.raw_data = response[:body] if response[:body].present? && !robject.conflict?
|
76
|
+
|
77
|
+
robject.conflict? ? robject.attempt_conflict_resolution : robject
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def extract_header(robject, response, name, attribute=nil, &block)
|
82
|
+
extract_if_present(robject, response[:headers], name, attribute) do |value|
|
83
|
+
block ? block.call(value[0]) : value[0]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def extract_if_present(robject, hash, key, attribute=nil)
|
88
|
+
if hash[key].present?
|
89
|
+
attribute ||= key
|
90
|
+
value = block_given? ? yield(hash[key]) : hash[key]
|
91
|
+
robject.send("#{attribute}=", value)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def extract_siblings(robject, data)
|
96
|
+
Util::Multipart.parse(data, Util::Multipart.extract_boundary(robject.content_type)).map do |part|
|
97
|
+
RObject.new(robject.bucket, robject.key) do |sibling|
|
98
|
+
load_object(sibling, part)
|
99
|
+
sibling.vclock = robject.vclock
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|