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.
@@ -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
@@ -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
@@ -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