aliyun-oss-ruby-sdk 0.4.1

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