oss 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +113 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/oss.rb +43 -0
- data/lib/oss/api.rb +342 -0
- data/lib/oss/bucket.rb +473 -0
- data/lib/oss/client.rb +206 -0
- data/lib/oss/cors.rb +106 -0
- data/lib/oss/error.rb +20 -0
- data/lib/oss/multipart.rb +225 -0
- data/lib/oss/object.rb +318 -0
- data/lib/oss/service.rb +63 -0
- data/lib/oss/util.rb +59 -0
- data/lib/oss/version.rb +3 -0
- data/oss.gemspec +26 -0
- metadata +119 -0
data/lib/oss/client.rb
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'openssl'
|
4
|
+
require 'base64'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'oss/error'
|
7
|
+
|
8
|
+
module Oss
|
9
|
+
|
10
|
+
class Client
|
11
|
+
|
12
|
+
attr_reader :endpoint, :access_key_id, :access_key_secret
|
13
|
+
attr_accessor :debug_mode
|
14
|
+
|
15
|
+
def initialize(endpoint, access_key_id, access_key_secret, debug_mode = false)
|
16
|
+
@debug_mode = debug_mode
|
17
|
+
@endpoint = endpoint
|
18
|
+
@access_key_id = access_key_id
|
19
|
+
@access_key_secret = access_key_secret
|
20
|
+
end
|
21
|
+
|
22
|
+
# http put request
|
23
|
+
def put(options = {})
|
24
|
+
request_prepare(:put, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# http get request
|
28
|
+
def get(options = {})
|
29
|
+
request_prepare(:get, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# http post request
|
33
|
+
def post(options = {})
|
34
|
+
request_prepare(:post, options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# http delete request
|
38
|
+
def delete(options = {})
|
39
|
+
request_prepare(:delete, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# http delete request
|
43
|
+
def head(options = {})
|
44
|
+
request_prepare(:head, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
# http delete request
|
48
|
+
def options(options = {})
|
49
|
+
request_prepare(:options, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def request_prepare(method, options)
|
53
|
+
# set header sign
|
54
|
+
options[:sign_configs] ||= Hash.new
|
55
|
+
options[:sign_configs][:date] = gmt_date
|
56
|
+
options[:sign_configs][:host] = options[:host]
|
57
|
+
options[:sign_configs][:path] = options[:path]
|
58
|
+
options[:sign_configs][:verb] = method.to_s.upcase
|
59
|
+
|
60
|
+
# content md5 check if need
|
61
|
+
if options[:content_md5_check]
|
62
|
+
# payload make md digest
|
63
|
+
md5 = Digest::MD5.digest(options[:payload])
|
64
|
+
|
65
|
+
# then make base64 and trim \n
|
66
|
+
cm = Base64.encode64(md5).gsub("\n", '')
|
67
|
+
options[:sign_configs][:content_md5] = cm
|
68
|
+
|
69
|
+
# set to header
|
70
|
+
options[:headers] ||= Hash.new
|
71
|
+
options[:headers]['Content-MD5'] = cm
|
72
|
+
end
|
73
|
+
|
74
|
+
# set header content length if need
|
75
|
+
if options[:content_length_check]
|
76
|
+
options[:payload] ||= ''
|
77
|
+
options[:headers]['Content-Length'] = options[:payload].bytesize
|
78
|
+
end
|
79
|
+
|
80
|
+
# set get path query string
|
81
|
+
path = Util.set_query_string(options[:path], options[:query_string])
|
82
|
+
|
83
|
+
# add query string to sign if need
|
84
|
+
if options[:sign_configs][:sign_query_string]
|
85
|
+
options[:sign_configs][:path] = path
|
86
|
+
end
|
87
|
+
|
88
|
+
# create sign header
|
89
|
+
signed_header = set_header_sign(options[:headers], options[:sign_configs])
|
90
|
+
|
91
|
+
# send request
|
92
|
+
request(options[:host], path, signed_header, options[:as] || :xml) do |url, header|
|
93
|
+
|
94
|
+
# use RestClient send request
|
95
|
+
RestClient::Request.execute(url: url, method: method, headers: header, payload: options[:payload])
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def request(host, path, headers, as = :xml, &block)
|
101
|
+
url = "http://#{host}#{path}"
|
102
|
+
|
103
|
+
# send request and rescue errors
|
104
|
+
begin
|
105
|
+
response = yield(url, headers)
|
106
|
+
rescue RestClient::ExceptionWithResponse => err
|
107
|
+
response = err.response
|
108
|
+
end
|
109
|
+
|
110
|
+
p "DEBUG: response body =======>\n#{response.body}\n" if debug_mode
|
111
|
+
|
112
|
+
if response.code/100 != 2
|
113
|
+
# error response
|
114
|
+
error_handler(response)
|
115
|
+
else
|
116
|
+
# success response
|
117
|
+
case as
|
118
|
+
when :xml
|
119
|
+
# parse as xml object
|
120
|
+
Nokogiri::XML(response.body)
|
121
|
+
when :raw
|
122
|
+
# return raw response
|
123
|
+
response
|
124
|
+
else
|
125
|
+
raise 'Undefined request as param!'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def error_handler(response)
|
135
|
+
xml_doc = Nokogiri::XML(response.body)
|
136
|
+
|
137
|
+
# parse all error infos
|
138
|
+
error_attrs = {
|
139
|
+
http_status: response.code,
|
140
|
+
code: xml_doc.xpath('Error/Code').inner_text,
|
141
|
+
message: xml_doc.xpath('Error/Message').inner_text,
|
142
|
+
header: xml_doc.xpath('Error/Header').inner_text,
|
143
|
+
request_id: xml_doc.xpath('Error/RequestId').inner_text,
|
144
|
+
host_id: xml_doc.xpath('Error/HostId').inner_text
|
145
|
+
}
|
146
|
+
|
147
|
+
# error raise format
|
148
|
+
status = "HttpStatus: #{error_attrs[:http_status]}\n"
|
149
|
+
code = "Code: #{error_attrs[:code]}\n"
|
150
|
+
message = "Message: #{error_attrs[:message]}\n"
|
151
|
+
header = error_attrs[:header] == '' ? '' : "Header: #{error_attrs[:header]}\n"
|
152
|
+
request_id = "RequestId: #{error_attrs[:request_id]}\n"
|
153
|
+
host = "HostId: #{error_attrs[:host_id]}\n"
|
154
|
+
|
155
|
+
error_info = "\n#{status}#{code}#{message}#{header}#{request_id}#{host}\n"
|
156
|
+
|
157
|
+
# raise error
|
158
|
+
raise ResponseError.new(error_attrs, error_info)
|
159
|
+
end
|
160
|
+
|
161
|
+
def set_header_sign(origin_header, configs = {})
|
162
|
+
# new hash if not exist
|
163
|
+
origin_header = Hash.new if origin_header.nil?
|
164
|
+
|
165
|
+
# set authorization signature header
|
166
|
+
origin_header['Authorization'] = signature(
|
167
|
+
configs[:verb],
|
168
|
+
configs[:date],
|
169
|
+
"#{configs[:resource]}#{configs[:path]}",
|
170
|
+
configs[:content_md5],
|
171
|
+
configs[:content_type],
|
172
|
+
configs[:oss_headers]
|
173
|
+
)
|
174
|
+
|
175
|
+
# set server headers
|
176
|
+
origin_header['Host'] = configs[:host]
|
177
|
+
origin_header['Date'] = configs[:date]
|
178
|
+
origin_header
|
179
|
+
end
|
180
|
+
|
181
|
+
def signature(verb, date, resource = '', content_md5 = '', content_type = '', oss_headers = '')
|
182
|
+
# set oss headers like x-oss-something into sign
|
183
|
+
oss_headers << "\n" if oss_headers != nil and oss_headers != ''
|
184
|
+
|
185
|
+
# string ready to sign
|
186
|
+
string_to_sign = "#{verb}\n#{content_md5}\n#{content_type}\n#{date}\n#{oss_headers}#{resource}"
|
187
|
+
|
188
|
+
p "DEBUG: client sign string =======>\n#{string_to_sign}\n" if debug_mode
|
189
|
+
|
190
|
+
# use hmac-sha1 then base64
|
191
|
+
digest = OpenSSL::Digest.new('sha1')
|
192
|
+
h = OpenSSL::HMAC.digest(digest, access_key_secret, string_to_sign)
|
193
|
+
b = Base64.encode64(h)
|
194
|
+
|
195
|
+
# final signature Authorization value
|
196
|
+
"OSS #{access_key_id}:#{b.gsub("\n", '')}"
|
197
|
+
end
|
198
|
+
|
199
|
+
# create GMT date format
|
200
|
+
def gmt_date
|
201
|
+
Time.now.gmtime.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
data/lib/oss/cors.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'oss/client'
|
2
|
+
require 'oss/util'
|
3
|
+
|
4
|
+
module Oss
|
5
|
+
|
6
|
+
class Cors
|
7
|
+
|
8
|
+
include Util
|
9
|
+
|
10
|
+
attr_accessor :client, :xml_obj
|
11
|
+
|
12
|
+
def initialize(client)
|
13
|
+
@client = client
|
14
|
+
end
|
15
|
+
|
16
|
+
def put_bucket_cors(bucket_name, rules)
|
17
|
+
# sign configs
|
18
|
+
sign_configs = Hash.new
|
19
|
+
sign_configs[:resource] = "/#{bucket_name}"
|
20
|
+
sign_configs[:content_type] = 'application/x-www-form-urlencoded'
|
21
|
+
sign_configs[:content_length_check] = true
|
22
|
+
|
23
|
+
# build payload xml
|
24
|
+
payload = Nokogiri::XML::Builder.new do |xml|
|
25
|
+
xml.CORSConfiguration do
|
26
|
+
rules.each do |rule|
|
27
|
+
xml.CORSRule do
|
28
|
+
rule.each do |a_rule|
|
29
|
+
xml.send(a_rule[:key], a_rule[:value])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
@xml_obj = client.put(
|
37
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
38
|
+
path: '/?cors',
|
39
|
+
sign_configs: sign_configs,
|
40
|
+
payload: payload.to_xml
|
41
|
+
)
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_bucket_cors(bucket_name)
|
47
|
+
@xml_obj = client.get(
|
48
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
49
|
+
sign_configs: {resource: "/#{bucket_name}"},
|
50
|
+
path: '/?cors',
|
51
|
+
)
|
52
|
+
|
53
|
+
rules = Array.new
|
54
|
+
@xml_obj.xpath('CORSConfiguration/CORSRule').each do |rule|
|
55
|
+
rules << rule
|
56
|
+
end
|
57
|
+
|
58
|
+
rules
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete_bucket_cors(bucket_name)
|
62
|
+
client.delete(
|
63
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
64
|
+
path: '/?cors',
|
65
|
+
sign_configs: {resource: "/#{bucket_name}"},
|
66
|
+
)
|
67
|
+
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def option_object(bucket_name, object_name, origin, request_method, request_headers)
|
72
|
+
# sign configs
|
73
|
+
sign_configs = Hash.new
|
74
|
+
sign_configs[:resource] = "/#{bucket_name}"
|
75
|
+
|
76
|
+
headers = Hash.new
|
77
|
+
headers['Origin'] = origin
|
78
|
+
headers['Access-Control-Request-Method'] = request_method
|
79
|
+
headers['Access-Control-Request-Headers'] = request_headers
|
80
|
+
|
81
|
+
resp = client.options(
|
82
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
83
|
+
path: "/#{object_name}",
|
84
|
+
sign_configs: sign_configs,
|
85
|
+
headers: headers,
|
86
|
+
as: :raw
|
87
|
+
)
|
88
|
+
|
89
|
+
# return response header
|
90
|
+
resp.headers
|
91
|
+
end
|
92
|
+
|
93
|
+
def method_missing(method)
|
94
|
+
if @xml_obj.nil?
|
95
|
+
super
|
96
|
+
else
|
97
|
+
camel = Util.camelize(method)
|
98
|
+
value = @xml_obj.xpath(camel)
|
99
|
+
raise "missing xml attribute #{camel}" if value.length == 0
|
100
|
+
value.inner_text
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
data/lib/oss/error.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Oss
|
2
|
+
|
3
|
+
class ResponseError < StandardError
|
4
|
+
|
5
|
+
attr_reader :http_status, :error_code, :error_message, :header, :request_id, :host_id
|
6
|
+
|
7
|
+
def initialize(err_attrs, err_msg)
|
8
|
+
@error_code = err_attrs[:code]
|
9
|
+
@error_message = err_attrs[:message]
|
10
|
+
@http_status = err_attrs[:http_status]
|
11
|
+
@header = err_attrs[:header]
|
12
|
+
@request_id = err_attrs[:request_id]
|
13
|
+
@host_id = err_attrs[:host_id]
|
14
|
+
|
15
|
+
super err_msg
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'oss/client'
|
2
|
+
require 'oss/util'
|
3
|
+
|
4
|
+
module Oss
|
5
|
+
|
6
|
+
class Multipart
|
7
|
+
|
8
|
+
include Util
|
9
|
+
|
10
|
+
attr_accessor :client, :xml_obj
|
11
|
+
|
12
|
+
def initialize(client)
|
13
|
+
@client = client
|
14
|
+
end
|
15
|
+
|
16
|
+
def initiate_multipart_upload(bucket_name, object_name, options = {})
|
17
|
+
# set header
|
18
|
+
headers = Util.hash_filter(options, {
|
19
|
+
expires: 'Expires',
|
20
|
+
content_control: 'Cache-Control',
|
21
|
+
content_encoding: 'Content-Encoding',
|
22
|
+
content_disposition: 'Content-Disposition',
|
23
|
+
encryption: 'x-oss-server-side-encryption',
|
24
|
+
})
|
25
|
+
|
26
|
+
# sign configs
|
27
|
+
sign_configs = {
|
28
|
+
resource: "/#{bucket_name}",
|
29
|
+
content_type: 'application/x-www-form-urlencoded',
|
30
|
+
oss_headers: Util.oss_headers_to_s(options, {
|
31
|
+
encryption: 'x-oss-server-side-encryption',
|
32
|
+
})
|
33
|
+
}
|
34
|
+
|
35
|
+
@xml_obj = client.post(
|
36
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
37
|
+
path: "/#{object_name}?uploads",
|
38
|
+
headers: headers,
|
39
|
+
sign_configs: sign_configs,
|
40
|
+
).xpath('InitiateMultipartUploadResult')
|
41
|
+
|
42
|
+
# return init info
|
43
|
+
{
|
44
|
+
bucket: @xml_obj.xpath('Bucket').text,
|
45
|
+
key: @xml_obj.xpath('Key').text,
|
46
|
+
upload_id: @xml_obj.xpath('UploadId').text,
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def upload_part(bucket_name, object_name, upload_id, file, part_number = 1, options = {})
|
51
|
+
# sign configs
|
52
|
+
sign_configs = {
|
53
|
+
resource: "/#{bucket_name}",
|
54
|
+
content_type: 'application/x-www-form-urlencoded',
|
55
|
+
content_length_check: true,
|
56
|
+
content_md5_check: options[:content_md5_check],
|
57
|
+
sign_query_string: true
|
58
|
+
}
|
59
|
+
|
60
|
+
resp = client.put(
|
61
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
62
|
+
path: "/#{object_name}",
|
63
|
+
sign_configs: sign_configs,
|
64
|
+
query_string: {'partNumber' => part_number, 'uploadId' => upload_id},
|
65
|
+
payload: file,
|
66
|
+
as: :raw
|
67
|
+
)
|
68
|
+
|
69
|
+
{etag: resp.headers[:etag], part_number: part_number}
|
70
|
+
end
|
71
|
+
|
72
|
+
def upload_part_copy(bucket_name, object_name, upload_id, old_bucket, old_object, part_number = 1, options = {})
|
73
|
+
# copy source format
|
74
|
+
options[:copy_source] = "/#{old_bucket}/#{old_object}"
|
75
|
+
options[:copy_source_range] = "bytes=#{options[:copy_source_begin]}-#{options[:copy_source_end]}" unless options[:copy_source_begin].nil?
|
76
|
+
|
77
|
+
# set header
|
78
|
+
headers = Util.hash_filter(options, {
|
79
|
+
if_modified_since: 'x-oss-copy-source-if-modified-since',
|
80
|
+
if_unmodified_since: 'x-oss-copy-source-if-unmodified-since',
|
81
|
+
if_match: 'x-oss-copy-source-if-match',
|
82
|
+
if_none_match: 'x-oss-copy-source-if-none-match',
|
83
|
+
copy_source: 'x-oss-copy-source',
|
84
|
+
copy_source_range: 'x-oss-copy-source-range'
|
85
|
+
})
|
86
|
+
|
87
|
+
# sign configs
|
88
|
+
sign_configs = {
|
89
|
+
resource: "/#{bucket_name}",
|
90
|
+
sign_query_string: true,
|
91
|
+
content_type: 'application/x-www-form-urlencoded',
|
92
|
+
oss_headers: Util.oss_headers_to_s(options, {
|
93
|
+
if_modified_since: 'x-oss-copy-source-if-modified-since',
|
94
|
+
if_unmodified_since: 'x-oss-copy-source-if-unmodified-since',
|
95
|
+
if_match: 'x-oss-copy-source-if-match',
|
96
|
+
if_none_match: 'x-oss-copy-source-if-none-match',
|
97
|
+
copy_source: 'x-oss-copy-source',
|
98
|
+
copy_source_range: 'x-oss-copy-source-range'
|
99
|
+
})
|
100
|
+
}
|
101
|
+
|
102
|
+
@xml_obj = client.put(
|
103
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
104
|
+
path: "/#{object_name}",
|
105
|
+
headers: headers,
|
106
|
+
sign_configs: sign_configs,
|
107
|
+
query_string: {'partNumber' => part_number, 'uploadId' => upload_id},
|
108
|
+
)
|
109
|
+
|
110
|
+
{
|
111
|
+
last_modify: @xml_obj.xpath('CopyPartResult/LastModified').text,
|
112
|
+
etag: @xml_obj.xpath('CopyPartResult/ETag').text,
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def complete_multipart_upload(bucket_name, object_name, upload_id, parts)
|
117
|
+
# build payload xml
|
118
|
+
payload = Nokogiri::XML::Builder.new do
|
119
|
+
CompleteMultipartUpload do
|
120
|
+
parts.each do |part|
|
121
|
+
Part do
|
122
|
+
PartNumber part[:part_number]
|
123
|
+
ETag part[:etag]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
@xml_obj = client.post(
|
130
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
131
|
+
path: "/#{object_name}",
|
132
|
+
sign_configs: {resource: "/#{bucket_name}", sign_query_string: true, content_type: 'application/x-www-form-urlencoded',},
|
133
|
+
query_string: {'uploadId' => upload_id},
|
134
|
+
payload: payload.to_xml
|
135
|
+
).xpath('CompleteMultipartUploadResult')
|
136
|
+
|
137
|
+
# return object info
|
138
|
+
{
|
139
|
+
location: @xml_obj.xpath('Location').text,
|
140
|
+
bucket: @xml_obj.xpath('Bucket').text,
|
141
|
+
key: @xml_obj.xpath('Key').text,
|
142
|
+
etag: @xml_obj.xpath('ETag').text,
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
def abort_multipart_upload(bucket_name, object_name, upload_id)
|
147
|
+
|
148
|
+
client.delete(
|
149
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
150
|
+
path: "/#{object_name}",
|
151
|
+
sign_configs: {resource: "/#{bucket_name}", sign_query_string: true},
|
152
|
+
query_string: {'uploadId' => upload_id},
|
153
|
+
)
|
154
|
+
|
155
|
+
true
|
156
|
+
end
|
157
|
+
|
158
|
+
def list_multipart_upload(bucket_name, options = {})
|
159
|
+
@xml_obj = client.get(
|
160
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
161
|
+
sign_configs: {resource: "/#{bucket_name}"},
|
162
|
+
path: '/?uploads',
|
163
|
+
query_string: options
|
164
|
+
).xpath('ListMultipartUploadsResult')
|
165
|
+
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
def uploads
|
170
|
+
uploads = @xml_obj.xpath('Upload')
|
171
|
+
results = Array.new
|
172
|
+
|
173
|
+
# parse as array
|
174
|
+
uploads.each do |upload|
|
175
|
+
results << {
|
176
|
+
key: upload.xpath('Key').text,
|
177
|
+
upload_id: upload.xpath('UploadId').text,
|
178
|
+
initiated: upload.xpath('Initiated').text,
|
179
|
+
}
|
180
|
+
end
|
181
|
+
results
|
182
|
+
end
|
183
|
+
|
184
|
+
def list_parts(bucket_name, object_name, options = {})
|
185
|
+
@xml_obj = client.get(
|
186
|
+
host: "#{bucket_name}.#{client.endpoint}",
|
187
|
+
path: "/#{object_name}",
|
188
|
+
query_string: options,
|
189
|
+
sign_configs: {resource: "/#{bucket_name}", sign_query_string: true},
|
190
|
+
).xpath('ListPartsResult')
|
191
|
+
|
192
|
+
self
|
193
|
+
end
|
194
|
+
|
195
|
+
def parts
|
196
|
+
parts = @xml_obj.xpath('Part')
|
197
|
+
results = Array.new
|
198
|
+
|
199
|
+
# parse as array
|
200
|
+
parts.each do |part|
|
201
|
+
results << {
|
202
|
+
part_number: part.xpath('PartNumber').text,
|
203
|
+
last_modified: part.xpath('LastModified').text,
|
204
|
+
etag: part.xpath('ETag').text,
|
205
|
+
size: part.xpath('Size').text,
|
206
|
+
}
|
207
|
+
end
|
208
|
+
results
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
def method_missing(method)
|
213
|
+
if @xml_obj.nil?
|
214
|
+
super
|
215
|
+
else
|
216
|
+
camel = Util.camelize(method)
|
217
|
+
value = @xml_obj.xpath(camel)
|
218
|
+
raise "missing xml attribute #{camel}" if value.length == 0
|
219
|
+
value.inner_text
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|