oss-ruby-sdk 0.1.0

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