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
data/lib/riak/search.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
module Riak
|
2
|
+
module Serializers
|
3
|
+
include Util::Translation
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def [](content_type)
|
7
|
+
serializers[content_type]
|
8
|
+
end
|
9
|
+
|
10
|
+
def []=(content_type, serializer)
|
11
|
+
serializers[content_type] = serializer
|
12
|
+
end
|
13
|
+
|
14
|
+
def serialize(content_type, content)
|
15
|
+
serializer_for(content_type).dump(content)
|
16
|
+
end
|
17
|
+
|
18
|
+
def deserialize(content_type, content)
|
19
|
+
serializer_for(content_type).load(content)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def serializer_for(content_type)
|
25
|
+
serializers.fetch(content_type[/^[^;\s]+/]) do
|
26
|
+
raise NotImplementedError.new(t('serializer_not_implemented', :content_type => content_type.inspect))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def serializers
|
31
|
+
@serializers ||= {}
|
32
|
+
end
|
33
|
+
|
34
|
+
module TextPlain
|
35
|
+
extend self
|
36
|
+
|
37
|
+
def dump(object)
|
38
|
+
object.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
def load(string)
|
42
|
+
string
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module ApplicationJSON
|
47
|
+
extend self
|
48
|
+
|
49
|
+
def dump(object)
|
50
|
+
object.to_json(Riak.json_options)
|
51
|
+
end
|
52
|
+
|
53
|
+
def load(string)
|
54
|
+
Riak::JSON.parse(string)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Serializers['text/plain'] = TextPlain
|
59
|
+
Serializers['application/json'] = ApplicationJSON
|
60
|
+
Serializers['application/x-ruby-marshal'] = ::Marshal
|
61
|
+
|
62
|
+
YAML_MIME_TYPES = %w[
|
63
|
+
text/yaml
|
64
|
+
text/x-yaml
|
65
|
+
application/yaml
|
66
|
+
application/x-yaml
|
67
|
+
]
|
68
|
+
|
69
|
+
YAML_MIME_TYPES.each do |mime_type|
|
70
|
+
Serializers[mime_type] = ::YAML
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
data/lib/riak/stamp.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'riak/client'
|
2
|
+
require 'riak/util/translation'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module Riak
|
6
|
+
# Implements a client-side form of monotonically-increasing k-sorted
|
7
|
+
# unique identifiers. These are useful for key generation if your
|
8
|
+
# data is time-sequential and needs to be sorted by key, perhaps in
|
9
|
+
# Riak Search. Inspired by Twitter's Snowflake project.
|
10
|
+
class Stamp
|
11
|
+
attr_reader :client
|
12
|
+
|
13
|
+
CLIENT_ID_MASK = (1 << 10) - 1
|
14
|
+
SEQUENCE_MASK = (1 << 12) - 1
|
15
|
+
TIMESTAMP_MASK = (1 << 41) - 1
|
16
|
+
SEQUENCE_SHIFT = 10
|
17
|
+
TIMESTAMP_SHIFT = 22
|
18
|
+
|
19
|
+
# @param [Client] client a {Riak::Client} which will be used for
|
20
|
+
# the "worker ID" component of the stamp.
|
21
|
+
# @see Client#stamp
|
22
|
+
def initialize(client)
|
23
|
+
@client = client
|
24
|
+
@mutex = Mutex.new
|
25
|
+
@timestamp = time_gen
|
26
|
+
@sequence = 0
|
27
|
+
end
|
28
|
+
|
29
|
+
# Generates a k-sorted unique ID for use as a key or other
|
30
|
+
# disambiguation purposes.
|
31
|
+
def next
|
32
|
+
@mutex.synchronize do
|
33
|
+
now = time_gen
|
34
|
+
if @timestamp == now
|
35
|
+
@sequence = (@sequence + 1) & SEQUENCE_MASK
|
36
|
+
now = wait_for_next_ms(@timestamp) if @sequence == 0
|
37
|
+
else
|
38
|
+
@sequence = 0
|
39
|
+
end
|
40
|
+
|
41
|
+
raise BackwardsClockError.new(@timestamp - now) if now < @timestamp
|
42
|
+
|
43
|
+
@timestamp = now
|
44
|
+
@timestamp << TIMESTAMP_SHIFT | @sequence << SEQUENCE_SHIFT | client_id
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def client_id
|
50
|
+
case id = @client.client_id
|
51
|
+
when Integer
|
52
|
+
id & CLIENT_ID_MASK
|
53
|
+
else
|
54
|
+
id.hash & CLIENT_ID_MASK
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def time_gen
|
59
|
+
(Time.now.to_f * 1000).floor & TIMESTAMP_MASK
|
60
|
+
end
|
61
|
+
|
62
|
+
def wait_for_next_ms(start)
|
63
|
+
now = time_gen
|
64
|
+
now = time_gen while now <= start
|
65
|
+
now
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Raised when calling {Stamp#next} and NTP or some other external
|
70
|
+
# event has moved the system clock backwards.
|
71
|
+
class BackwardsClockError < StandardError
|
72
|
+
include Util::Translation
|
73
|
+
def initialize(delay)
|
74
|
+
super t('backwards_clock', :delay => delay)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'riak/node'
|
2
|
+
|
3
|
+
if ENV['DEBUG_RIAK_TEST_SERVER']
|
4
|
+
$expect_verbose = true
|
5
|
+
end
|
6
|
+
|
7
|
+
module Riak
|
8
|
+
# The TestServer is a special {Node} that uses in-memory storage
|
9
|
+
# engines that are easily cleared. This is helpful when running test
|
10
|
+
# suites that store and retrieve objects from Riak and expect a
|
11
|
+
# clean-slate at the beginning of each test. Like {Node}, creation
|
12
|
+
# is idempotent, so you can keep the server around between runs of
|
13
|
+
# your test suite.
|
14
|
+
class TestServer < Node
|
15
|
+
# Creates a TestServer node, using in-memory backends for KV and Search.
|
16
|
+
def initialize(configuration = {})
|
17
|
+
configuration[:env] ||= {}
|
18
|
+
configuration[:env][:riak_kv] ||= {}
|
19
|
+
(configuration[:env][:riak_kv][:add_paths] ||= []) << File.expand_path("../../../erl_src", __FILE__)
|
20
|
+
configuration[:env][:riak_kv][:test] = true
|
21
|
+
configuration[:env][:memory_backend] ||={}
|
22
|
+
configuration[:env][:memory_backend][:test] = true
|
23
|
+
configuration[:env][:riak_search] ||= {}
|
24
|
+
configuration[:env][:riak_search][:search_backend] = :riak_search_test_backend
|
25
|
+
super configuration
|
26
|
+
end
|
27
|
+
|
28
|
+
# Overrides the default {Node#started?} to simply return true if the
|
29
|
+
# console is still attached.
|
30
|
+
def started?
|
31
|
+
open? || super
|
32
|
+
end
|
33
|
+
|
34
|
+
# Overrides the default {Node#start} to return early if the
|
35
|
+
# console is still attached. Otherwise, starts and immediately
|
36
|
+
# attaches the console.
|
37
|
+
def start
|
38
|
+
unless open?
|
39
|
+
super
|
40
|
+
maybe_attach
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Overrides the default {Node#stop} to close the console before
|
45
|
+
# stopping the node.
|
46
|
+
def stop
|
47
|
+
@console.close if @console && !@console.frozen?
|
48
|
+
@console = nil
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
# Overrides the default {Node#drop} to simply clear the in-memory
|
53
|
+
# backends.
|
54
|
+
def drop
|
55
|
+
begin
|
56
|
+
maybe_attach
|
57
|
+
@console.command "#{kv_backend}:reset()."
|
58
|
+
@console.command "riak_search_test_backend:reset()."
|
59
|
+
rescue IOError
|
60
|
+
retry
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
# Tries to reattach the console if it's closed
|
66
|
+
def maybe_attach
|
67
|
+
unless open?
|
68
|
+
@console.close if @console && !@console.frozen?
|
69
|
+
@console = attach
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def open?
|
74
|
+
@console && @console.open?
|
75
|
+
end
|
76
|
+
|
77
|
+
def configure_data
|
78
|
+
super
|
79
|
+
if version < "1.0.0"
|
80
|
+
env[:riak_kv][:storage_backend] = :riak_kv_test014_backend
|
81
|
+
elsif version =~ /^1\.[01]\.\d+$/ # 1.0 and 1.1 series
|
82
|
+
env[:riak_kv][:storage_backend] = :riak_kv_test_backend
|
83
|
+
else
|
84
|
+
# TODO: change this when 1.2+ is released, if it includes riak_kv#314
|
85
|
+
env[:riak_kv][:storage_backend] = :riak_kv_memory_backend
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Riak
|
5
|
+
class << self
|
6
|
+
# @see #escaper=
|
7
|
+
attr_reader :escaper
|
8
|
+
|
9
|
+
# Sets the class used for escaping URLs (buckets and keys) sent to
|
10
|
+
# Riak. Currently only supports URI and CGI, and defaults to URI.
|
11
|
+
# @param [Symbol,String,Class] esc A representation of which
|
12
|
+
# escaping class to use, either the Class itself or a String or
|
13
|
+
# Symbol name
|
14
|
+
# @see Riak::Util::Escape
|
15
|
+
def escaper=(esc)
|
16
|
+
case esc
|
17
|
+
when Symbol, String
|
18
|
+
@escaper = ::Object.const_get(esc.to_s.upcase.intern) if esc.to_s =~ /uri|cgi/i
|
19
|
+
when Class, Module
|
20
|
+
@escaper = esc
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# In Riak 1.0+, buckets and keys are decoded internally before
|
25
|
+
# being stored. This increases compatibility with the Protocol
|
26
|
+
# Buffers transport and reduces inconsistency of link-walking
|
27
|
+
# vs. regular operations. If the node you are connecting to has
|
28
|
+
# set {http_url_encoding, on}, set this to true. Default is false.
|
29
|
+
# @return [true,false] Whether Riak decodes URL-encoded paths and headers
|
30
|
+
attr_accessor :url_decoding
|
31
|
+
end
|
32
|
+
|
33
|
+
self.escaper = URI
|
34
|
+
self.url_decoding = false
|
35
|
+
|
36
|
+
module Util
|
37
|
+
# Methods for escaping URL segments.
|
38
|
+
module Escape
|
39
|
+
# Conditionally escapes buckets and keys depending on whether
|
40
|
+
# Riak is configured to decode them. This is used in situations
|
41
|
+
# where the bucket or key is not part of a URL, but would need
|
42
|
+
# to be escaped on Riak 0.14 and earlier so that the name
|
43
|
+
# matches.
|
44
|
+
# @param [String] bucket_or_key the bucket or key name
|
45
|
+
# @return [String] the escaped path segment
|
46
|
+
def maybe_escape(bucket_or_key)
|
47
|
+
Riak.url_decoding ? bucket_or_key : escape(bucket_or_key)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Escapes bucket or key names that may contain slashes for use in URLs.
|
51
|
+
# @param [String] bucket_or_key the bucket or key name
|
52
|
+
# @return [String] the escaped path segment
|
53
|
+
def escape(bucket_or_key)
|
54
|
+
Riak.escaper.escape(bucket_or_key.to_s).gsub("+", "%20").gsub('/', "%2F")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Conditionally unescapes buckets and keys depending on whether
|
58
|
+
# Riak is configured to decode them. This is used in situations
|
59
|
+
# where the bucket or key is not part of a URL, but would need
|
60
|
+
# to be escaped on Riak 0.14 and earlier so that the name
|
61
|
+
# matches.
|
62
|
+
# @param [String] bucket_or_key the escaped bucket or key name
|
63
|
+
# @return [String] the unescaped path segment
|
64
|
+
def maybe_unescape(bucket_or_key)
|
65
|
+
Riak.url_decoding ? bucket_or_key : unescape(bucket_or_key)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Unescapes bucket or key names in URLs.
|
69
|
+
# @param [String] bucket_or_key the bucket or key name
|
70
|
+
# @return [String] the unescaped name
|
71
|
+
def unescape(bucket_or_key)
|
72
|
+
Riak.escaper.unescape(bucket_or_key.to_s)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
# Splits headers into < 8KB chunks
|
4
|
+
# @private
|
5
|
+
module Net::HTTPHeader
|
6
|
+
def each_capitalized
|
7
|
+
# 1.9 check
|
8
|
+
respond_to?(:enum_for) and (block_given? or return enum_for(__method__))
|
9
|
+
@header.each do |k,v|
|
10
|
+
base_length = "#{k}: \r\n".length
|
11
|
+
values = v.map {|i| i.to_s.split(", ") }.flatten
|
12
|
+
while !values.empty?
|
13
|
+
current_line = ""
|
14
|
+
while values.first && current_line.length + base_length + values.first.length + 2 < 8192
|
15
|
+
val = values.shift.strip
|
16
|
+
current_line += current_line.empty? ? val : ", #{val}"
|
17
|
+
end
|
18
|
+
yield capitalize(k), current_line
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module Riak
|
25
|
+
module Util
|
26
|
+
# Represents headers from an HTTP request or response.
|
27
|
+
# Used internally by HTTP backends for processing headers.
|
28
|
+
class Headers
|
29
|
+
include Net::HTTPHeader
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
initialize_http_header({})
|
33
|
+
end
|
34
|
+
|
35
|
+
# Parse a single header line into its key and value
|
36
|
+
# @param [String] chunk a single header line
|
37
|
+
def self.parse(chunk)
|
38
|
+
line = chunk.strip
|
39
|
+
# thanks Net::HTTPResponse
|
40
|
+
return [nil,nil] if chunk =~ /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in
|
41
|
+
m = /\A([^:]+):\s*/.match(line)
|
42
|
+
[m[1], m.post_match] rescue [nil, nil]
|
43
|
+
end
|
44
|
+
|
45
|
+
# Parses a header line and adds it to the header collection
|
46
|
+
# @param [String] chunk a single header line
|
47
|
+
def parse(chunk)
|
48
|
+
key, value = self.class.parse(chunk)
|
49
|
+
add_field(key, value) if key && value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'riak/util/translation'
|
2
|
+
require 'riak/util/multipart'
|
3
|
+
|
4
|
+
module Riak
|
5
|
+
module Util
|
6
|
+
module Multipart
|
7
|
+
# This class is parses chunked/streamed multipart HTTP
|
8
|
+
# streams. It is used by streaming MapReduce queries, and in the
|
9
|
+
# future streaming key-lists (once implemented on the Riak side).
|
10
|
+
class StreamParser
|
11
|
+
include Multipart
|
12
|
+
include Translation
|
13
|
+
# Creates a new StreamParser.
|
14
|
+
#
|
15
|
+
# Example usage:
|
16
|
+
# http.get(200, "/riak", "foo", {}, &StreamParser.new {|part| ... })
|
17
|
+
#
|
18
|
+
# @yield [Hash] parts of the multipart/mixed stream,
|
19
|
+
# containing :headers and :body keys
|
20
|
+
def initialize(&block)
|
21
|
+
raise ArgumentError, t('missing_block') unless block_given?
|
22
|
+
@buffer = ""
|
23
|
+
@block = block
|
24
|
+
@state = :get_boundary
|
25
|
+
end
|
26
|
+
|
27
|
+
# Accepts a chunk of the HTTP response stream, and yields to
|
28
|
+
# the block when appropriate.
|
29
|
+
def accept(chunk)
|
30
|
+
@buffer << chunk
|
31
|
+
@state = send(@state)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns a Proc that can be passed to an HTTP request method.
|
35
|
+
def to_proc
|
36
|
+
method(:accept).to_proc
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
CAPTURE_BOUNDARY = /^--([A-Za-z0-9\'()+_,-.\/:=?]+)\r?\n/
|
41
|
+
|
42
|
+
def get_boundary
|
43
|
+
if @buffer =~ CAPTURE_BOUNDARY
|
44
|
+
@re = /\r?\n--#{Regexp.escape($1)}(?:--)?\r?\n/
|
45
|
+
@buffer = $~.post_match
|
46
|
+
buffering
|
47
|
+
else
|
48
|
+
:get_boundary
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def buffering
|
53
|
+
while @buffer =~ @re
|
54
|
+
@block.call parse_multipart_section($~.pre_match)
|
55
|
+
@buffer = $~.post_match
|
56
|
+
end
|
57
|
+
:buffering
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'riak/util/headers'
|
2
|
+
|
3
|
+
module Riak
|
4
|
+
module Util
|
5
|
+
# Utility methods for handling multipart/mixed responses
|
6
|
+
module Multipart
|
7
|
+
extend self
|
8
|
+
# Parses a multipart/mixed body into its constituent parts, including nested multipart/mixed sections
|
9
|
+
# @param [String] data the multipart body data
|
10
|
+
# @param [String] boundary the boundary string given in the Content-Type header
|
11
|
+
def parse(data, boundary)
|
12
|
+
contents = data.match(end_boundary_regex(boundary)).pre_match rescue ""
|
13
|
+
contents.split(inner_boundary_regex(boundary)).reject(&:blank?).map do |part|
|
14
|
+
parse_multipart_section(part)
|
15
|
+
end.compact
|
16
|
+
end
|
17
|
+
|
18
|
+
# Extracts the boundary string from a Content-Type header that is a multipart type
|
19
|
+
# @param [String] header_string the Content-Type header
|
20
|
+
# @return [String] the boundary string separating each part
|
21
|
+
def extract_boundary(header_string)
|
22
|
+
$1 if header_string =~ /boundary=([A-Za-z0-9\'()+_,-.\/:=?]+)/
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def end_boundary_regex(boundary)
|
27
|
+
/\r?\n--#{Regexp.escape(boundary)}--\r?\n/
|
28
|
+
end
|
29
|
+
|
30
|
+
def inner_boundary_regex(boundary)
|
31
|
+
/\r?\n--#{Regexp.escape(boundary)}\r?\n/
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_multipart_section(part)
|
35
|
+
headers = Headers.new
|
36
|
+
if md = part.match(/\r?\n\r?\n/)
|
37
|
+
body = md.post_match
|
38
|
+
md.pre_match.split(/\r?\n/).each do |line|
|
39
|
+
headers.parse(line)
|
40
|
+
end
|
41
|
+
|
42
|
+
if headers["content-type"] =~ /multipart\/mixed/
|
43
|
+
boundary = extract_boundary(headers.to_hash["content-type"].first)
|
44
|
+
parse(body, boundary)
|
45
|
+
else
|
46
|
+
{:headers => headers.to_hash, :body => body}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|