aliyun-oss-ruby-sdk 0.4.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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +95 -0
  3. data/README.md +423 -0
  4. data/examples/aliyun/oss/bucket.rb +144 -0
  5. data/examples/aliyun/oss/callback.rb +61 -0
  6. data/examples/aliyun/oss/object.rb +182 -0
  7. data/examples/aliyun/oss/resumable_download.rb +42 -0
  8. data/examples/aliyun/oss/resumable_upload.rb +49 -0
  9. data/examples/aliyun/oss/streaming.rb +124 -0
  10. data/examples/aliyun/oss/using_sts.rb +48 -0
  11. data/examples/aliyun/sts/assume_role.rb +59 -0
  12. data/lib/aliyun_sdk/common.rb +6 -0
  13. data/lib/aliyun_sdk/common/exception.rb +18 -0
  14. data/lib/aliyun_sdk/common/logging.rb +46 -0
  15. data/lib/aliyun_sdk/common/struct.rb +56 -0
  16. data/lib/aliyun_sdk/oss.rb +16 -0
  17. data/lib/aliyun_sdk/oss/bucket.rb +661 -0
  18. data/lib/aliyun_sdk/oss/client.rb +106 -0
  19. data/lib/aliyun_sdk/oss/config.rb +39 -0
  20. data/lib/aliyun_sdk/oss/download.rb +255 -0
  21. data/lib/aliyun_sdk/oss/exception.rb +108 -0
  22. data/lib/aliyun_sdk/oss/http.rb +338 -0
  23. data/lib/aliyun_sdk/oss/iterator.rb +92 -0
  24. data/lib/aliyun_sdk/oss/multipart.rb +74 -0
  25. data/lib/aliyun_sdk/oss/object.rb +15 -0
  26. data/lib/aliyun_sdk/oss/protocol.rb +1499 -0
  27. data/lib/aliyun_sdk/oss/struct.rb +208 -0
  28. data/lib/aliyun_sdk/oss/upload.rb +238 -0
  29. data/lib/aliyun_sdk/oss/util.rb +89 -0
  30. data/lib/aliyun_sdk/sts.rb +9 -0
  31. data/lib/aliyun_sdk/sts/client.rb +38 -0
  32. data/lib/aliyun_sdk/sts/config.rb +22 -0
  33. data/lib/aliyun_sdk/sts/exception.rb +53 -0
  34. data/lib/aliyun_sdk/sts/protocol.rb +130 -0
  35. data/lib/aliyun_sdk/sts/struct.rb +64 -0
  36. data/lib/aliyun_sdk/sts/util.rb +48 -0
  37. data/lib/aliyun_sdk/version.rb +7 -0
  38. data/spec/aliyun/oss/bucket_spec.rb +597 -0
  39. data/spec/aliyun/oss/client/bucket_spec.rb +554 -0
  40. data/spec/aliyun/oss/client/client_spec.rb +297 -0
  41. data/spec/aliyun/oss/client/resumable_download_spec.rb +220 -0
  42. data/spec/aliyun/oss/client/resumable_upload_spec.rb +413 -0
  43. data/spec/aliyun/oss/http_spec.rb +83 -0
  44. data/spec/aliyun/oss/multipart_spec.rb +686 -0
  45. data/spec/aliyun/oss/object_spec.rb +785 -0
  46. data/spec/aliyun/oss/service_spec.rb +142 -0
  47. data/spec/aliyun/oss/util_spec.rb +50 -0
  48. data/spec/aliyun/sts/client_spec.rb +150 -0
  49. data/spec/aliyun/sts/util_spec.rb +39 -0
  50. data/tests/config.rb +31 -0
  51. data/tests/test_content_encoding.rb +54 -0
  52. data/tests/test_content_type.rb +95 -0
  53. data/tests/test_custom_headers.rb +70 -0
  54. data/tests/test_encoding.rb +77 -0
  55. data/tests/test_large_file.rb +66 -0
  56. data/tests/test_multipart.rb +97 -0
  57. data/tests/test_object_acl.rb +49 -0
  58. data/tests/test_object_key.rb +68 -0
  59. data/tests/test_object_url.rb +69 -0
  60. data/tests/test_resumable.rb +40 -0
  61. metadata +240 -0
@@ -0,0 +1,338 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'rest-client'
4
+ require 'resolv'
5
+ require 'fiber'
6
+
7
+ module AliyunSDK
8
+ module OSS
9
+
10
+ ##
11
+ # HTTP wraps the HTTP functionalities for accessing OSS RESTful
12
+ # API. It handles the OSS-specific protocol elements, and
13
+ # rest-client details for the user, which includes:
14
+ # * automatically generate signature for every request
15
+ # * parse response headers/body
16
+ # * raise exceptions and capture the request id
17
+ # * encapsulates streaming upload/download
18
+ # @example simple get
19
+ # headers, body = http.get({:bucket => 'bucket'})
20
+ # @example streaming download
21
+ # http.get({:bucket => 'bucket', :object => 'object'}) do |chunk|
22
+ # # handle chunk
23
+ # end
24
+ # @example streaming upload
25
+ # def streaming_upload(&block)
26
+ # http.put({:bucket => 'bucket', :object => 'object'},
27
+ # {:body => HTTP::StreamPlayload.new(block)})
28
+ # end
29
+ #
30
+ # streaming_upload do |stream|
31
+ # stream << "hello world"
32
+ # end
33
+ class HTTP
34
+
35
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'
36
+ DEFAULT_ACCEPT_ENCODING = 'identity'
37
+ STS_HEADER = 'x-oss-security-token'
38
+ OPEN_TIMEOUT = 10
39
+ READ_TIMEOUT = 120
40
+
41
+ ##
42
+ # A stream implementation
43
+ # A stream is any class that responds to :read(bytes, outbuf)
44
+ #
45
+ class StreamWriter
46
+ def initialize
47
+ @buffer = ""
48
+ @producer = Fiber.new { yield self if block_given? }
49
+ @producer.resume
50
+ end
51
+
52
+ def read(bytes = nil, outbuf = nil)
53
+ ret = ""
54
+ loop do
55
+ if bytes
56
+ fail if bytes < 0
57
+ piece = @buffer.slice!(0, bytes)
58
+ if piece
59
+ ret << piece
60
+ bytes -= piece.size
61
+ break if bytes == 0
62
+ end
63
+ else
64
+ ret << @buffer
65
+ @buffer.clear
66
+ end
67
+
68
+ if @producer.alive?
69
+ @producer.resume
70
+ else
71
+ break
72
+ end
73
+ end
74
+
75
+ if outbuf
76
+ # WARNING: Using outbuf = '' here DOES NOT work!
77
+ outbuf.clear
78
+ outbuf << ret
79
+ end
80
+
81
+ # Conform to IO#read(length[, outbuf]):
82
+ # At end of file, it returns nil or "" depend on
83
+ # length. ios.read() and ios.read(nil) returns
84
+ # "". ios.read(positive-integer) returns nil.
85
+ return nil if ret.empty? && !bytes.nil? && bytes > 0
86
+
87
+ ret
88
+ end
89
+
90
+ def write(chunk)
91
+ @buffer << chunk.to_s.force_encoding(Encoding::ASCII_8BIT)
92
+ Fiber.yield
93
+ self
94
+ end
95
+
96
+ alias << write
97
+
98
+ def closed?
99
+ false
100
+ end
101
+
102
+ def inspect
103
+ "@buffer: " + @buffer[0, 32].inspect + "...#{@buffer.size} bytes"
104
+ end
105
+ end
106
+
107
+ # RestClient requires the payload to respones to :read(bytes)
108
+ # and return a stream.
109
+ # We are not doing the real read here, just return a
110
+ # readable stream for RestClient playload.rb treats it as:
111
+ # def read(bytes=nil)
112
+ # @stream.read(bytes)
113
+ # end
114
+ # alias :to_s :read
115
+ # net_http_do_request(http, req, payload ? payload.to_s : nil,
116
+ # &@block_response)
117
+ class StreamPayload
118
+ def initialize(&block)
119
+ @stream = StreamWriter.new(&block)
120
+ end
121
+
122
+ def read(bytes = nil)
123
+ @stream
124
+ end
125
+
126
+ def close
127
+ end
128
+
129
+ def closed?
130
+ false
131
+ end
132
+
133
+ end
134
+
135
+ include Common::Logging
136
+
137
+ def initialize(config)
138
+ @config = config
139
+ end
140
+
141
+ def get_request_url(bucket, object)
142
+ url = @config.endpoint.dup
143
+ isIP = !!(url.host =~ Resolv::IPv4::Regex)
144
+ url.host = "#{bucket}." + url.host if bucket && !@config.cname && !isIP
145
+ url.path = '/'
146
+ url.path << "#{bucket}/" if bucket && isIP
147
+ url.path << "#{CGI.escape(object)}" if object
148
+
149
+ url.to_s
150
+ end
151
+
152
+ def get_resource_path(bucket, object)
153
+ res = '/'
154
+ res << "#{bucket}/" if bucket
155
+ res << "#{object}" if object
156
+
157
+ res
158
+ end
159
+
160
+ # Handle Net::HTTPRespoonse
161
+ def handle_response(r, &block)
162
+ # read all body on error
163
+ if r.code.to_i >= 300
164
+ r.read_body
165
+ else
166
+ # streaming read body on success
167
+ encoding = r['content-encoding']
168
+ if encoding == 'gzip'
169
+ stream = StreamWriter.new { |s| r.read_body { |chunk| s << chunk } }
170
+ reader = Zlib::GzipReader.new(stream)
171
+ yield reader.read(16 * 1024) until reader.eof?
172
+ elsif encoding == 'deflate'
173
+ begin
174
+ stream = Zlib::Inflate.new
175
+ # 1.9.x doesn't support streaming inflate
176
+ if RUBY_VERSION < '2.0.0'
177
+ yield stream.inflate(r.read_body)
178
+ else
179
+ r.read_body { |chunk| stream << chunk }
180
+ stream.finish { |chunk| yield chunk }
181
+ end
182
+ rescue Zlib::DataError
183
+ # No luck with Zlib decompression. Let's try with raw deflate,
184
+ # like some broken web servers do.
185
+ stream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
186
+ # 1.9.x doesn't support streaming inflate
187
+ if RUBY_VERSION < '2.0.0'
188
+ yield stream.inflate(r.read_body)
189
+ else
190
+ r.read_body { |chunk| stream << chunk }
191
+ stream.finish { |chunk| yield chunk }
192
+ end
193
+ end
194
+ else
195
+ r.read_body { |chunk| yield chunk }
196
+ end
197
+ end
198
+ end
199
+
200
+ ##
201
+ # helper methods
202
+ #
203
+ def get(resources = {}, http_options = {}, &block)
204
+ do_request('GET', resources, http_options, &block)
205
+ end
206
+
207
+ def put(resources = {}, http_options = {}, &block)
208
+ do_request('PUT', resources, http_options, &block)
209
+ end
210
+
211
+ def post(resources = {}, http_options = {}, &block)
212
+ do_request('POST', resources, http_options, &block)
213
+ end
214
+
215
+ def delete(resources = {}, http_options = {}, &block)
216
+ do_request('DELETE', resources, http_options, &block)
217
+ end
218
+
219
+ def head(resources = {}, http_options = {}, &block)
220
+ do_request('HEAD', resources, http_options, &block)
221
+ end
222
+
223
+ def options(resources = {}, http_options = {}, &block)
224
+ do_request('OPTIONS', resources, http_options, &block)
225
+ end
226
+
227
+ private
228
+ # Do HTTP reqeust
229
+ # @param verb [String] HTTP Verb: GET/PUT/POST/DELETE/HEAD/OPTIONS
230
+ # @param resources [Hash] OSS related resources
231
+ # @option resources [String] :bucket the bucket name
232
+ # @option resources [String] :object the object name
233
+ # @option resources [Hash] :sub_res sub-resources
234
+ # @param http_options [Hash] HTTP options
235
+ # @option http_options [Hash] :headers HTTP headers
236
+ # @option http_options [Hash] :query HTTP queries
237
+ # @option http_options [Object] :body HTTP body, may be String
238
+ # or Stream
239
+ def do_request(verb, resources = {}, http_options = {}, &block)
240
+ bucket = resources[:bucket]
241
+ object = resources[:object]
242
+ sub_res = resources[:sub_res]
243
+
244
+ headers = http_options[:headers] || {}
245
+ headers['user-agent'] = get_user_agent
246
+ headers['date'] = Time.now.httpdate
247
+ headers['content-type'] ||= DEFAULT_CONTENT_TYPE
248
+ headers['accept-encoding'] ||= DEFAULT_ACCEPT_ENCODING
249
+ headers[STS_HEADER] = @config.sts_token if @config.sts_token
250
+
251
+ if body = http_options[:body]
252
+ if body.respond_to?(:read)
253
+ headers['transfer-encoding'] = 'chunked'
254
+ else
255
+ headers['content-md5'] = Util.get_content_md5(body)
256
+ end
257
+ end
258
+
259
+ res = {
260
+ :path => get_resource_path(bucket, object),
261
+ :sub_res => sub_res,
262
+ }
263
+
264
+ if @config.access_key_id and @config.access_key_secret
265
+ sig = Util.get_signature(@config.access_key_secret, verb, headers, res)
266
+ headers['authorization'] = "OSS #{@config.access_key_id}:#{sig}"
267
+ end
268
+
269
+ logger.debug("Send HTTP request, verb: #{verb}, resources: " \
270
+ "#{resources}, http options: #{http_options}")
271
+
272
+ # From rest-client:
273
+ # "Due to unfortunate choices in the original API, the params
274
+ # used to populate the query string are actually taken out of
275
+ # the headers hash."
276
+ headers[:params] = (sub_res || {}).merge(http_options[:query] || {})
277
+
278
+ block_response = ->(r) { handle_response(r, &block) } if block
279
+ r = RestClient::Request.execute(
280
+ :method => verb,
281
+ :url => get_request_url(bucket, object),
282
+ :headers => headers,
283
+ :payload => http_options[:body],
284
+ :block_response => block_response,
285
+ :open_timeout => @config.open_timeout || OPEN_TIMEOUT,
286
+ :timeout => @config.read_timeout || READ_TIMEOUT
287
+ ) do |response, request, result, &blk|
288
+
289
+ if response.code >= 300
290
+ e = ServerError.new(response)
291
+ logger.error(e.to_s)
292
+ raise e
293
+ else
294
+ response.return!(request, result, &blk)
295
+ end
296
+ end
297
+
298
+ # If streaming read_body is used, we need to create the
299
+ # RestClient::Response ourselves
300
+ unless r.is_a?(RestClient::Response)
301
+ if r.code.to_i >= 300
302
+ r = RestClient::Response.create(
303
+ RestClient::Request.decode(r['content-encoding'], r.body),
304
+ r, nil, nil)
305
+ e = ServerError.new(r)
306
+ logger.error(e.to_s)
307
+ raise e
308
+ end
309
+ r = RestClient::Response.create(nil, r, nil, nil)
310
+ r.return!
311
+ end
312
+
313
+ logger.debug("Received HTTP response, code: #{r.code}, headers: " \
314
+ "#{r.headers}, body: #{r.body}")
315
+
316
+ r
317
+ end
318
+
319
+ def get_user_agent
320
+ "aliyun-sdk-ruby/#{VERSION} ruby-#{RUBY_VERSION}/#{RUBY_PLATFORM}"
321
+ end
322
+
323
+ end # HTTP
324
+ end # OSS
325
+ end # Aliyun
326
+
327
+ # Monkey patch rest-client to exclude the 'Content-Length' header when
328
+ # 'Transfer-Encoding' is set to 'chuncked'. This may be a problem for
329
+ # some http servers like tengine.
330
+ module RestClient
331
+ module Payload
332
+ class Base
333
+ def headers
334
+ ({'content-length' => size.to_s} if size) || {}
335
+ end
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,92 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module AliyunSDK
4
+ module OSS
5
+ ##
6
+ # Iterator structs that wrap the multiple communications with the
7
+ # server to provide an iterable result.
8
+ #
9
+ module Iterator
10
+
11
+ ##
12
+ # Iterator base that stores fetched results and fetch more if needed.
13
+ #
14
+ class Base
15
+ def initialize(protocol, opts = {})
16
+ @protocol = protocol
17
+ @results, @more = [], opts
18
+ end
19
+
20
+ def next
21
+ loop do
22
+ # Communicate with the server to get more results
23
+ fetch_more if @results.empty?
24
+
25
+ # Return the first result
26
+ r = @results.shift
27
+ break unless r
28
+
29
+ yield r
30
+ end
31
+ end
32
+
33
+ def to_enum
34
+ self.enum_for(:next)
35
+ end
36
+
37
+ private
38
+ def fetch_more
39
+ return if @more[:truncated] == false
40
+ fetch(@more)
41
+ end
42
+ end # Base
43
+
44
+ ##
45
+ # Buckets iterator
46
+ #
47
+ class Buckets < Base
48
+ def fetch(more)
49
+ @results, cont = @protocol.list_buckets(more)
50
+ @more[:marker] = cont[:next_marker]
51
+ @more[:truncated] = cont[:truncated] || false
52
+ end
53
+ end # Buckets
54
+
55
+ ##
56
+ # Objects iterator
57
+ #
58
+ class Objects < Base
59
+ def initialize(protocol, bucket_name, opts = {})
60
+ super(protocol, opts)
61
+ @bucket = bucket_name
62
+ end
63
+
64
+ def fetch(more)
65
+ @results, cont = @protocol.list_objects(@bucket, more)
66
+ @results = cont[:common_prefixes] + @results if cont[:common_prefixes]
67
+ @more[:marker] = cont[:next_marker]
68
+ @more[:truncated] = cont[:truncated] || false
69
+ end
70
+ end # Objects
71
+
72
+ ##
73
+ # Uploads iterator
74
+ #
75
+ class Uploads < Base
76
+ def initialize(protocol, bucket_name, opts = {})
77
+ super(protocol, opts)
78
+ @bucket = bucket_name
79
+ end
80
+
81
+ def fetch(more)
82
+ @results, cont = @protocol.list_multipart_uploads(@bucket, more)
83
+ @results = cont[:common_prefixes] + @results if cont[:common_prefixes]
84
+ @more[:id_marker] = cont[:next_id_marker]
85
+ @more[:key_marker] = cont[:next_key_marker]
86
+ @more[:truncated] = cont[:truncated] || false
87
+ end
88
+ end # Objects
89
+
90
+ end # Iterator
91
+ end # OSS
92
+ end # Aliyun
@@ -0,0 +1,74 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'json'
4
+ require 'digest/md5'
5
+
6
+ module AliyunSDK
7
+ module OSS
8
+
9
+ ##
10
+ # Multipart upload/download structures
11
+ #
12
+ module Multipart
13
+
14
+ ##
15
+ # A multipart transaction. Provide the basic checkpoint methods.
16
+ #
17
+ class Transaction < Common::Struct::Base
18
+
19
+ attrs :id, :object, :bucket, :creation_time, :options
20
+
21
+ def initialize(opts = {})
22
+ super(opts)
23
+
24
+ @mutex = Mutex.new
25
+ end
26
+
27
+ private
28
+ # Persist transaction states to file
29
+ def write_checkpoint(states, file)
30
+ md5= Util.get_content_md5(states.to_json)
31
+
32
+ @mutex.synchronize {
33
+ File.open(file, 'w') {
34
+ |f| f.write(states.merge(md5: md5).to_json)
35
+ }
36
+ }
37
+ end
38
+
39
+ # Load transaction states from file
40
+ def load_checkpoint(file)
41
+ states = {}
42
+
43
+ @mutex.synchronize {
44
+ states = JSON.load(File.read(file))
45
+ }
46
+ states = Util.symbolize(states)
47
+ md5 = states.delete(:md5)
48
+
49
+ fail CheckpointBrokenError, "Missing MD5 in checkpoint." unless md5
50
+ unless md5 == Util.get_content_md5(states.to_json)
51
+ fail CheckpointBrokenError, "Unmatched checkpoint MD5."
52
+ end
53
+
54
+ states
55
+ end
56
+
57
+ def get_file_md5(file)
58
+ Digest::MD5.file(file).to_s
59
+ end
60
+
61
+ end # Transaction
62
+
63
+ ##
64
+ # A part in a multipart uploading transaction
65
+ #
66
+ class Part < Common::Struct::Base
67
+
68
+ attrs :number, :etag, :size, :last_modified
69
+
70
+ end # Part
71
+
72
+ end # Multipart
73
+ end # OSS
74
+ end # Aliyun