raws 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +12 -4
- data/Rakefile +3 -1
- data/VERSION +1 -1
- data/lib/raws/http/ht2p.rb +83 -0
- data/lib/raws/http/typhoeus.rb +92 -0
- data/lib/raws/http.rb +25 -0
- data/lib/raws/s3/adapter.rb +187 -75
- data/lib/raws/s3/model.rb +71 -0
- data/lib/raws/s3/s3object.rb +3 -0
- data/lib/raws/s3.rb +115 -5
- data/lib/raws/sdb/adapter.rb +78 -13
- data/lib/raws/sdb/model.rb +1 -1
- data/lib/raws/sdb.rb +6 -0
- data/lib/raws/sqs/adapter.rb +90 -28
- data/lib/raws/sqs.rb +6 -0
- data/lib/raws/xml/nokogiri.rb +48 -0
- data/lib/raws/xml.rb +28 -0
- data/lib/raws.rb +30 -131
- data/spec/raws/s3_spec.rb +212 -21
- data/spec/raws/sdb_spec.rb +1 -1
- data/spec/raws/sqs_spec.rb +16 -7
- data/spec/raws_spec.rb +13 -29
- data/spec/spec_helper.rb +15 -0
- metadata +22 -4
data/README.rdoc
CHANGED
@@ -45,13 +45,11 @@ RAWS is a Ruby library for Amazon Web Service (AWS).
|
|
45
45
|
p RAWS::SDB['test_domain'].get('2')
|
46
46
|
p RAWS::SDB['test_domain'].get('3')
|
47
47
|
|
48
|
-
RAWS::SDB['test_domain'].all.each do |
|
49
|
-
key, data = a
|
48
|
+
RAWS::SDB['test_domain'].all.each do |key, data|
|
50
49
|
p [key, data]
|
51
50
|
end
|
52
51
|
|
53
|
-
RAWS::SDB['test_domain'].all.filter('a = ?', 10).each do |
|
54
|
-
key, data = a
|
52
|
+
RAWS::SDB['test_domain'].all.filter('a = ?', 10).each do |key, data|
|
55
53
|
p [key, data]
|
56
54
|
end
|
57
55
|
|
@@ -106,3 +104,13 @@ RAWS is a Ruby library for Amazon Web Service (AWS).
|
|
106
104
|
Foo.delete_domain
|
107
105
|
|
108
106
|
== S3 (Amazon Simple Storage Service)
|
107
|
+
|
108
|
+
require 'rubygems'
|
109
|
+
require 'raws'
|
110
|
+
|
111
|
+
RAWS.aws_access_key_id = _AWS_ACCESS_KEY_ID_
|
112
|
+
RAWS.aws_secret_access_key = _AWS_SECRET_ACCESS_KEY_
|
113
|
+
|
114
|
+
RAWS::S3.create_bucket('test_bucket')
|
115
|
+
|
116
|
+
RAWS::S3['test_bucket'].delete_bucket
|
data/Rakefile
CHANGED
@@ -18,10 +18,12 @@ begin
|
|
18
18
|
) + Dir.glob("{bin,doc,lib}/**/*")
|
19
19
|
s.require_path = "lib"
|
20
20
|
s.has_rdoc = true
|
21
|
-
s.add_dependency('
|
21
|
+
s.add_dependency('typhoeus', '>=0.1.9')
|
22
|
+
s.add_dependency('ht2p', '>=0.0.5')
|
22
23
|
s.add_dependency('nokogiri', '>=1.3.3')
|
23
24
|
s.add_dependency('uuidtools', '>=2.0.0')
|
24
25
|
end
|
26
|
+
Jeweler::GemcutterTasks.new
|
25
27
|
rescue LoadError
|
26
28
|
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
27
29
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.9
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'ht2p'
|
2
|
+
|
3
|
+
module RAWS::HTTP::HT2P
|
4
|
+
def self.connect(uri, &block)
|
5
|
+
response = nil
|
6
|
+
begin
|
7
|
+
HT2P::Client.new uri do |request|
|
8
|
+
response = block.call(Request.new(request))
|
9
|
+
end
|
10
|
+
rescue RAWS::HTTP::Redirect => e
|
11
|
+
r = e.response
|
12
|
+
uri = r.header['location'] || r.doc['Error']['Endpoint']
|
13
|
+
retry
|
14
|
+
end
|
15
|
+
response
|
16
|
+
end
|
17
|
+
|
18
|
+
class Request < RAWS::HTTP::Request
|
19
|
+
def initialize(request)
|
20
|
+
@request, @before_send = request, nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def method
|
24
|
+
@request.method
|
25
|
+
end
|
26
|
+
|
27
|
+
def method=(val)
|
28
|
+
@request.method = val
|
29
|
+
end
|
30
|
+
|
31
|
+
def header
|
32
|
+
@request.header
|
33
|
+
end
|
34
|
+
|
35
|
+
def before_send(&block)
|
36
|
+
@before_send = block
|
37
|
+
end
|
38
|
+
|
39
|
+
def send(body=nil, &block)
|
40
|
+
RAWS.logger.debug self
|
41
|
+
@before_send && @before_send.call(self)
|
42
|
+
response = Response.new(@request.send(body, &block))
|
43
|
+
case response.code
|
44
|
+
when 200...300
|
45
|
+
response
|
46
|
+
when 300...400
|
47
|
+
response.parse
|
48
|
+
raise RAWS::HTTP::Redirect.new(response)
|
49
|
+
else
|
50
|
+
response.parse
|
51
|
+
raise RAWS::HTTP::Error.new(response)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Response < RAWS::HTTP::Response
|
57
|
+
attr_reader :body, :doc
|
58
|
+
|
59
|
+
def initialize(response)
|
60
|
+
@response, @body, @doc = response, nil, nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def code
|
64
|
+
@response.code
|
65
|
+
end
|
66
|
+
|
67
|
+
def header
|
68
|
+
@response.header
|
69
|
+
end
|
70
|
+
|
71
|
+
def receive(&block)
|
72
|
+
if block_given?
|
73
|
+
@response.receive(&block)
|
74
|
+
else
|
75
|
+
@body = @response.receive
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse(params={})
|
80
|
+
@doc = RAWS.xml.parse(receive, params)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'typhoeus'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
class RAWS::HTTP::Typhoeus
|
5
|
+
def self.connect(uri, &block)
|
6
|
+
response = nil
|
7
|
+
begin
|
8
|
+
response = block.call(Request.new(uri))
|
9
|
+
rescue RAWS::HTTP::Redirect => e
|
10
|
+
r = e.response
|
11
|
+
uri = r.header['location'] || r.doc['Error']['Endpoint']
|
12
|
+
retry
|
13
|
+
end
|
14
|
+
response
|
15
|
+
end
|
16
|
+
|
17
|
+
class Request < RAWS::HTTP::Request
|
18
|
+
attr_reader :header
|
19
|
+
attr_accessor :uri, :method
|
20
|
+
|
21
|
+
def initialize(uri)
|
22
|
+
@uri, @header, @method, @before_send = uri, {}, :get , nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def before_send(&block)
|
26
|
+
@before_send = block
|
27
|
+
end
|
28
|
+
|
29
|
+
def send(body=nil, &block)
|
30
|
+
RAWS.logger.debug self
|
31
|
+
@before_send && @before_send.call(self)
|
32
|
+
response = Response.new(
|
33
|
+
::Typhoeus::Request.__send__(
|
34
|
+
@method.downcase.to_sym,
|
35
|
+
@uri,
|
36
|
+
:headers => @header,
|
37
|
+
:body => if block_given?
|
38
|
+
# TODO エラーにした方が。。。
|
39
|
+
io = StringIO.new
|
40
|
+
block.call(io)
|
41
|
+
if io.size > 0
|
42
|
+
io.rewind
|
43
|
+
io.read
|
44
|
+
end
|
45
|
+
else
|
46
|
+
body
|
47
|
+
end
|
48
|
+
)
|
49
|
+
)
|
50
|
+
case response.code
|
51
|
+
when 200...300
|
52
|
+
response
|
53
|
+
when 300...400
|
54
|
+
response.parse
|
55
|
+
raise RAWS::HTTP::Redirect.new(response)
|
56
|
+
else
|
57
|
+
response.parse
|
58
|
+
raise RAWS::HTTP::Error.new(response)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Response < RAWS::HTTP::Response
|
64
|
+
attr_reader :body, :doc
|
65
|
+
|
66
|
+
def initialize(response)
|
67
|
+
@response, @body, @doc = response, nil, nil
|
68
|
+
@header = @response.headers.split("\r\n").inject({}) do |ret, val|
|
69
|
+
if md = /(.+?):\s*(.*)/.match(val)
|
70
|
+
ret[md[1].downcase] = md[2]
|
71
|
+
end
|
72
|
+
ret
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def code
|
77
|
+
@response.code
|
78
|
+
end
|
79
|
+
|
80
|
+
def receive(&block)
|
81
|
+
if block_given?
|
82
|
+
block.call(StringIO.new(@response.body)) # TODO エラーにした方が。。。
|
83
|
+
else
|
84
|
+
@body = @response.body
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse(params={})
|
89
|
+
@doc = RAWS.xml.parse(receive, params)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/raws/http.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module RAWS
|
2
|
+
module HTTP
|
3
|
+
class Redirect < Exception
|
4
|
+
attr_reader :response
|
5
|
+
|
6
|
+
def initialize(response)
|
7
|
+
@response = response
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Error < StandardError
|
12
|
+
attr_reader :response
|
13
|
+
|
14
|
+
def initialize(response)
|
15
|
+
@response = response
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Request; end
|
20
|
+
class Response; end
|
21
|
+
|
22
|
+
autoload :Typhoeus, 'raws/http/typhoeus'
|
23
|
+
autoload :HT2P, 'raws/http/ht2p'
|
24
|
+
end
|
25
|
+
end
|
data/lib/raws/s3/adapter.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'digest/md5'
|
2
|
-
|
3
1
|
class RAWS::S3::Adapter
|
4
2
|
module Adapter20060301
|
5
3
|
URI_PARAMS = {
|
@@ -9,113 +7,227 @@ class RAWS::S3::Adapter
|
|
9
7
|
:query => {}
|
10
8
|
}
|
11
9
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
def parse_params(params)
|
11
|
+
params = URI_PARAMS.merge(params)
|
12
|
+
|
13
|
+
path = if bucket = params[:bucket]
|
14
|
+
if bucket.include?('.')
|
15
|
+
params.delete :bucket
|
16
|
+
params[:path] = "/#{bucket}#{params[:path]}"
|
16
17
|
else
|
17
|
-
params[:
|
18
|
+
"/#{bucket}#{params[:path]}"
|
18
19
|
end
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
else
|
21
|
+
params[:path]
|
22
|
+
end
|
23
|
+
|
24
|
+
unless params[:query].empty?
|
25
|
+
params[:path] += '?' << params[:query].map do |key, val|
|
26
|
+
val ? "#{RAWS.escape(key)}=#{RAWS.escape(val)}" : RAWS.escape(key)
|
27
|
+
end.sort.join(';')
|
28
|
+
|
29
|
+
params[:query].each do |key, val|
|
30
|
+
unless val
|
31
|
+
path << "?#{key}"
|
32
|
+
break
|
33
|
+
end
|
26
34
|
end
|
27
|
-
|
35
|
+
end
|
36
|
+
|
37
|
+
[
|
38
|
+
"#{params[:scheme]}://#{
|
39
|
+
if params[:bucket]
|
40
|
+
"#{params[:bucket]}.#{params[:host]}"
|
41
|
+
else
|
42
|
+
params[:host]
|
43
|
+
end
|
44
|
+
}#{params[:path]}",
|
45
|
+
path
|
46
|
+
]
|
28
47
|
end
|
48
|
+
private :parse_params
|
29
49
|
|
30
|
-
def sign(
|
31
|
-
"#{RAWS.aws_access_key_id}:#{
|
50
|
+
def sign(request, path)
|
51
|
+
request.header['authorization'] = "AWS #{RAWS.aws_access_key_id}:#{
|
32
52
|
[
|
33
53
|
::OpenSSL::HMAC.digest(
|
34
54
|
::OpenSSL::Digest::SHA1.new,
|
35
55
|
RAWS.aws_secret_access_key,
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
56
|
+
(
|
57
|
+
[
|
58
|
+
request.method,
|
59
|
+
request.header['content-md5'],
|
60
|
+
request.header['content-type'],
|
61
|
+
request.header['x-amz-date'] ? '' : request.header['date'],
|
62
|
+
] + request.header.select do |key, val|
|
63
|
+
/^x-amz-/.match(key)
|
64
|
+
end.sort.map do |key, val|
|
65
|
+
"#{key}:#{val}"
|
66
|
+
end + [
|
67
|
+
path
|
68
|
+
]
|
69
|
+
).join("\n")
|
43
70
|
)
|
44
71
|
].pack('m').strip
|
45
72
|
}"
|
46
73
|
end
|
74
|
+
private :sign
|
47
75
|
|
48
|
-
def
|
49
|
-
|
50
|
-
uri
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
sign(http_verb, content, type, date, uri[:path])
|
58
|
-
}"
|
59
|
-
}
|
60
|
-
)
|
61
|
-
data = RAWS.parse(Nokogiri::XML.parse(r.body), options)
|
62
|
-
if 200 <= r.code && r.code <= 299
|
63
|
-
data
|
64
|
-
else
|
65
|
-
raise RAWS::Error.new(r, data)
|
76
|
+
def connect(method, params={}, &block)
|
77
|
+
uri, path = parse_params params
|
78
|
+
RAWS::S3.http.connect(uri) do |request|
|
79
|
+
request.method = method
|
80
|
+
request.header['date'] = Time.now.httpdate
|
81
|
+
request.before_send do |_request|
|
82
|
+
sign _request, path
|
83
|
+
end
|
84
|
+
block.call request
|
66
85
|
end
|
67
86
|
end
|
68
87
|
|
69
88
|
def get_service
|
70
|
-
|
89
|
+
connect 'GET' do |request|
|
90
|
+
response = request.send
|
91
|
+
response.parse :multiple => ['Bucket']
|
92
|
+
response
|
93
|
+
end
|
71
94
|
end
|
72
95
|
|
73
|
-
def put_bucket(bucket_name)
|
74
|
-
|
96
|
+
def put_bucket(bucket_name, location=nil, header={})
|
97
|
+
connect 'PUT', :bucket => bucket_name do |request|
|
98
|
+
request.header.merge! header
|
99
|
+
response = request.send(
|
100
|
+
if location
|
101
|
+
"<CreateBucketConfiguration>"
|
102
|
+
"<LocationConstraint>#{
|
103
|
+
location
|
104
|
+
}</LocationConstraint>"
|
105
|
+
"</CreateBucketConfiguration>"
|
106
|
+
end
|
107
|
+
)
|
108
|
+
response.receive
|
109
|
+
response
|
110
|
+
end
|
75
111
|
end
|
76
112
|
|
77
|
-
def put_request_payment(bucket_name)
|
78
|
-
|
79
|
-
'PUT',
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
113
|
+
def put_request_payment(bucket_name, requester)
|
114
|
+
connect(
|
115
|
+
'PUT',
|
116
|
+
:bucket => bucket_name,
|
117
|
+
:query => {'requestPayment' => nil}
|
118
|
+
) do |request|
|
119
|
+
response = request.send\
|
120
|
+
'<RequestPaymentConfiguration'
|
121
|
+
' xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
|
122
|
+
"<Payer>#{requester}</Payer>"
|
123
|
+
'</RequestPaymentConfiguration>'
|
124
|
+
response.receive
|
125
|
+
response
|
126
|
+
end
|
85
127
|
end
|
86
128
|
|
87
129
|
def get_bucket(bucket_name, params={})
|
88
|
-
|
89
|
-
'GET',
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
130
|
+
connect(
|
131
|
+
'GET',
|
132
|
+
:bucket => bucket_name,
|
133
|
+
:query => params
|
134
|
+
) do |request|
|
135
|
+
response = request.send
|
136
|
+
response.parse :multiple => ['Contents']
|
137
|
+
response
|
138
|
+
end
|
95
139
|
end
|
96
140
|
|
97
141
|
def get_request_payment(bucket_name)
|
98
|
-
|
99
|
-
'GET',
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
142
|
+
connect(
|
143
|
+
'GET',
|
144
|
+
:bucket => bucket_name,
|
145
|
+
:query => {'requestPayment' => nil}
|
146
|
+
) do |request|
|
147
|
+
response = request.send
|
148
|
+
response.parse
|
149
|
+
response
|
150
|
+
end
|
105
151
|
end
|
106
152
|
|
107
153
|
def get_bucket_location(bucket_name)
|
108
|
-
|
109
|
-
'GET',
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
154
|
+
connect(
|
155
|
+
'GET',
|
156
|
+
:bucket => bucket_name,
|
157
|
+
:query => {'location' => nil}
|
158
|
+
) do |request|
|
159
|
+
response = request.send
|
160
|
+
response.parse
|
161
|
+
response
|
162
|
+
end
|
115
163
|
end
|
116
164
|
|
117
165
|
def delete_bucket(bucket_name)
|
118
|
-
|
166
|
+
connect 'DELETE', :bucket => bucket_name do |request|
|
167
|
+
request.send
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def put_object(bucket_name, name, header, &block)
|
172
|
+
connect(
|
173
|
+
'PUT',
|
174
|
+
:bucket => bucket_name,
|
175
|
+
:path => '/' << name
|
176
|
+
) do |request|
|
177
|
+
request.header.merge!(header)
|
178
|
+
response = request.send(&block)
|
179
|
+
response.receive
|
180
|
+
response
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def copy_object(src_bucket, src_name, dest_bucket, dest_name, header={})
|
185
|
+
connect(
|
186
|
+
'PUT',
|
187
|
+
:bucket => dest_bucket,
|
188
|
+
:path => '/' << dest_name
|
189
|
+
) do |request|
|
190
|
+
request.header.merge! header
|
191
|
+
request.header['x-amz-copy-source'] = "/#{src_bucket}/#{src_name}"
|
192
|
+
request.send
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def get_object(bucket_name, name=nil, header={}, &block)
|
197
|
+
connect(
|
198
|
+
'GET',
|
199
|
+
:bucket => bucket_name,
|
200
|
+
:path => "/#{name}"
|
201
|
+
) do |request|
|
202
|
+
request.header.merge! header
|
203
|
+
response = request.send
|
204
|
+
response.receive(&block)
|
205
|
+
response
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def head_object(bucket_name, name)
|
210
|
+
connect(
|
211
|
+
'HEAD',
|
212
|
+
:bucket => bucket_name,
|
213
|
+
:path => '/' << name
|
214
|
+
) do |request|
|
215
|
+
response = request.send
|
216
|
+
response.receive
|
217
|
+
response
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def delete_object(bucket_name, name)
|
222
|
+
connect(
|
223
|
+
'DELETE',
|
224
|
+
:bucket => bucket_name,
|
225
|
+
:path => '/' << name
|
226
|
+
) do |request|
|
227
|
+
response = request.send
|
228
|
+
response.receive
|
229
|
+
response
|
230
|
+
end
|
119
231
|
end
|
120
232
|
end
|
121
233
|
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module RAWS::S3::Model
|
2
|
+
module ClassMethods
|
3
|
+
attr_accessor :bucket_name
|
4
|
+
|
5
|
+
def create_bucket
|
6
|
+
RAWS::S3.create_bucket(self.bucket_name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def delete_bucket(force=nil)
|
10
|
+
RAWS::S3.delete_bucket(self.bucket_name, force)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module InstanceMethods
|
15
|
+
def after_initialize; end
|
16
|
+
def before_delete; end
|
17
|
+
def after_delete; end
|
18
|
+
def before_save; end
|
19
|
+
def after_save; end
|
20
|
+
def before_update; end
|
21
|
+
def after_update; end
|
22
|
+
def before_insert; end
|
23
|
+
def after_insert; end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.included(mod)
|
27
|
+
mod.class_eval do
|
28
|
+
include InstanceMethods
|
29
|
+
extend ClassMethods
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Metadata < Hash
|
34
|
+
X_AMZ_META = 'x-amz-meta-'
|
35
|
+
|
36
|
+
def initialize(object)
|
37
|
+
super()
|
38
|
+
@object = object
|
39
|
+
decode(@object.header)
|
40
|
+
end
|
41
|
+
|
42
|
+
def decode(header)
|
43
|
+
header.select do |key, val|
|
44
|
+
key.match(/^#{X_AMZ_META}/)
|
45
|
+
end.each do |key, val|
|
46
|
+
self[key.sub(X_AMZ_META, '')] = begin
|
47
|
+
a = val.split(',').map do |val|
|
48
|
+
RAWS.unescape(val)
|
49
|
+
end
|
50
|
+
1 < a.size ? a : a.first
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def encode
|
56
|
+
self.inject({}) do |ret, (key, val)|
|
57
|
+
key = X_AMZ_META + key
|
58
|
+
|
59
|
+
if val.is_a? Array
|
60
|
+
ret[key] = val.map do |v|
|
61
|
+
RAWS.escape(v.strip)
|
62
|
+
end.join(',')
|
63
|
+
else
|
64
|
+
ret[key] = RAWS.escape(val.strip)
|
65
|
+
end
|
66
|
+
|
67
|
+
ret
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|