better-riak-client 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|