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,15 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module AliyunSDK
4
+ module OSS
5
+
6
+ ##
7
+ # Object表示OSS存储的一个对象
8
+ #
9
+ class Object < Common::Struct::Base
10
+
11
+ attrs :key, :type, :size, :etag, :metas, :last_modified, :headers
12
+
13
+ end # Object
14
+ end # OSS
15
+ end # Aliyun
@@ -0,0 +1,1499 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'rest-client'
4
+ require 'nokogiri'
5
+ require 'time'
6
+
7
+ module AliyunSDK
8
+ module OSS
9
+
10
+ ##
11
+ # Protocol implement the OSS Open API which is low-level. User
12
+ # should refer to {OSS::Client} for normal use.
13
+ #
14
+ class Protocol
15
+
16
+ STREAM_CHUNK_SIZE = 16 * 1024
17
+ CALLBACK_HEADER = 'x-oss-callback'
18
+
19
+ include Common::Logging
20
+
21
+ def initialize(config)
22
+ @config = config
23
+ @http = HTTP.new(config)
24
+ end
25
+
26
+ # List all the buckets.
27
+ # @param opts [Hash] options
28
+ # @option opts [String] :prefix return only those buckets
29
+ # prefixed with it if specified
30
+ # @option opts [String] :marker return buckets after where it
31
+ # indicates (exclusively). All buckets are sorted by name
32
+ # alphabetically
33
+ # @option opts [Integer] :limit return only the first N
34
+ # buckets if specified
35
+ # @return [Array<Bucket>, Hash] the returned buckets and a
36
+ # hash including the next tokens, which includes:
37
+ # * :prefix [String] the prefix used
38
+ # * :delimiter [String] the delimiter used
39
+ # * :marker [String] the marker used
40
+ # * :limit [Integer] the limit used
41
+ # * :next_marker [String] marker to continue list buckets
42
+ # * :truncated [Boolean] whether there are more buckets to
43
+ # be returned
44
+ def list_buckets(opts = {})
45
+ logger.info("Begin list buckets, options: #{opts}")
46
+
47
+ params = {
48
+ 'prefix' => opts[:prefix],
49
+ 'marker' => opts[:marker],
50
+ 'max-keys' => opts[:limit]
51
+ }.reject { |_, v| v.nil? }
52
+
53
+ r = @http.get( {}, {:query => params})
54
+ doc = parse_xml(r.body)
55
+
56
+ buckets = doc.css("Buckets Bucket").map do |node|
57
+ Bucket.new(
58
+ {
59
+ :name => get_node_text(node, "Name"),
60
+ :location => get_node_text(node, "Location"),
61
+ :creation_time =>
62
+ get_node_text(node, "CreationDate") { |t| Time.parse(t) }
63
+ }, self
64
+ )
65
+ end
66
+
67
+ more = {
68
+ :prefix => 'Prefix',
69
+ :limit => 'MaxKeys',
70
+ :marker => 'Marker',
71
+ :next_marker => 'NextMarker',
72
+ :truncated => 'IsTruncated'
73
+ }.reduce({}) { |h, (k, v)|
74
+ value = get_node_text(doc.root, v)
75
+ value.nil?? h : h.merge(k => value)
76
+ }
77
+
78
+ update_if_exists(
79
+ more, {
80
+ :limit => ->(x) { x.to_i },
81
+ :truncated => ->(x) { x.to_bool }
82
+ }
83
+ )
84
+
85
+ logger.info("Done list buckets, buckets: #{buckets}, more: #{more}")
86
+
87
+ [buckets, more]
88
+ end
89
+
90
+ # Create a bucket
91
+ # @param name [String] the bucket name
92
+ # @param opts [Hash] options
93
+ # @option opts [String] :location the region where the bucket
94
+ # is located
95
+ # @example
96
+ # oss-cn-hangzhou
97
+ def create_bucket(name, opts = {})
98
+ logger.info("Begin create bucket, name: #{name}, opts: #{opts}")
99
+
100
+ location = opts[:location]
101
+ body = nil
102
+ if location
103
+ builder = Nokogiri::XML::Builder.new do |xml|
104
+ xml.CreateBucketConfiguration {
105
+ xml.LocationConstraint location
106
+ }
107
+ end
108
+ body = builder.to_xml
109
+ end
110
+
111
+ @http.put({:bucket => name}, {:body => body})
112
+
113
+ logger.info("Done create bucket")
114
+ end
115
+
116
+ # Put bucket acl
117
+ # @param name [String] the bucket name
118
+ # @param acl [String] the bucket acl
119
+ # @see OSS::ACL
120
+ def put_bucket_acl(name, acl)
121
+ logger.info("Begin put bucket acl, name: #{name}, acl: #{acl}")
122
+
123
+ sub_res = {'acl' => nil}
124
+ headers = {'x-oss-acl' => acl}
125
+ @http.put(
126
+ {:bucket => name, :sub_res => sub_res},
127
+ {:headers => headers, :body => nil})
128
+
129
+ logger.info("Done put bucket acl")
130
+ end
131
+
132
+ # Get bucket acl
133
+ # @param name [String] the bucket name
134
+ # @return [String] the acl of this bucket
135
+ def get_bucket_acl(name)
136
+ logger.info("Begin get bucket acl, name: #{name}")
137
+
138
+ sub_res = {'acl' => nil}
139
+ r = @http.get({:bucket => name, :sub_res => sub_res})
140
+
141
+ doc = parse_xml(r.body)
142
+ acl = get_node_text(doc.at_css("AccessControlList"), 'Grant')
143
+ logger.info("Done get bucket acl")
144
+
145
+ acl
146
+ end
147
+
148
+ # Put bucket logging settings
149
+ # @param name [String] the bucket name
150
+ # @param logging [BucketLogging] logging options
151
+ def put_bucket_logging(name, logging)
152
+ logger.info("Begin put bucket logging, "\
153
+ "name: #{name}, logging: #{logging}")
154
+
155
+ if logging.enabled? && !logging.target_bucket
156
+ fail ClientError,
157
+ "Must specify target bucket when enabling bucket logging."
158
+ end
159
+
160
+ sub_res = {'logging' => nil}
161
+ body = Nokogiri::XML::Builder.new do |xml|
162
+ xml.BucketLoggingStatus {
163
+ if logging.enabled?
164
+ xml.LoggingEnabled {
165
+ xml.TargetBucket logging.target_bucket
166
+ xml.TargetPrefix logging.target_prefix if logging.target_prefix
167
+ }
168
+ end
169
+ }
170
+ end.to_xml
171
+
172
+ @http.put(
173
+ {:bucket => name, :sub_res => sub_res},
174
+ {:body => body})
175
+
176
+ logger.info("Done put bucket logging")
177
+ end
178
+
179
+ # Get bucket logging settings
180
+ # @param name [String] the bucket name
181
+ # @return [BucketLogging] logging options of this bucket
182
+ def get_bucket_logging(name)
183
+ logger.info("Begin get bucket logging, name: #{name}")
184
+
185
+ sub_res = {'logging' => nil}
186
+ r = @http.get({:bucket => name, :sub_res => sub_res})
187
+
188
+ doc = parse_xml(r.body)
189
+ opts = {:enable => false}
190
+
191
+ logging_node = doc.at_css("LoggingEnabled")
192
+ opts.update(
193
+ :target_bucket => get_node_text(logging_node, 'TargetBucket'),
194
+ :target_prefix => get_node_text(logging_node, 'TargetPrefix')
195
+ )
196
+ opts[:enable] = true if opts[:target_bucket]
197
+
198
+ logger.info("Done get bucket logging")
199
+
200
+ BucketLogging.new(opts)
201
+ end
202
+
203
+ # Delete bucket logging settings, a.k.a. disable bucket logging
204
+ # @param name [String] the bucket name
205
+ def delete_bucket_logging(name)
206
+ logger.info("Begin delete bucket logging, name: #{name}")
207
+
208
+ sub_res = {'logging' => nil}
209
+ @http.delete({:bucket => name, :sub_res => sub_res})
210
+
211
+ logger.info("Done delete bucket logging")
212
+ end
213
+
214
+ # Put bucket website settings
215
+ # @param name [String] the bucket name
216
+ # @param website [BucketWebsite] the bucket website options
217
+ def put_bucket_website(name, website)
218
+ logger.info("Begin put bucket website, "\
219
+ "name: #{name}, website: #{website}")
220
+
221
+ unless website.index
222
+ fail ClientError, "Must specify index to put bucket website."
223
+ end
224
+
225
+ sub_res = {'website' => nil}
226
+ body = Nokogiri::XML::Builder.new do |xml|
227
+ xml.WebsiteConfiguration {
228
+ xml.IndexDocument {
229
+ xml.Suffix website.index
230
+ }
231
+ if website.error
232
+ xml.ErrorDocument {
233
+ xml.Key website.error
234
+ }
235
+ end
236
+ }
237
+ end.to_xml
238
+
239
+ @http.put(
240
+ {:bucket => name, :sub_res => sub_res},
241
+ {:body => body})
242
+
243
+ logger.info("Done put bucket website")
244
+ end
245
+
246
+ # Get bucket website settings
247
+ # @param name [String] the bucket name
248
+ # @return [BucketWebsite] the bucket website options
249
+ def get_bucket_website(name)
250
+ logger.info("Begin get bucket website, name: #{name}")
251
+
252
+ sub_res = {'website' => nil}
253
+ r = @http.get({:bucket => name, :sub_res => sub_res})
254
+
255
+ opts = {:enable => true}
256
+ doc = parse_xml(r.body)
257
+ opts.update(
258
+ :index => get_node_text(doc.at_css('IndexDocument'), 'Suffix'),
259
+ :error => get_node_text(doc.at_css('ErrorDocument'), 'Key')
260
+ )
261
+
262
+ logger.info("Done get bucket website")
263
+
264
+ BucketWebsite.new(opts)
265
+ end
266
+
267
+ # Delete bucket website settings
268
+ # @param name [String] the bucket name
269
+ def delete_bucket_website(name)
270
+ logger.info("Begin delete bucket website, name: #{name}")
271
+
272
+ sub_res = {'website' => nil}
273
+ @http.delete({:bucket => name, :sub_res => sub_res})
274
+
275
+ logger.info("Done delete bucket website")
276
+ end
277
+
278
+ # Put bucket referer
279
+ # @param name [String] the bucket name
280
+ # @param referer [BucketReferer] the bucket referer options
281
+ def put_bucket_referer(name, referer)
282
+ logger.info("Begin put bucket referer, "\
283
+ "name: #{name}, referer: #{referer}")
284
+
285
+ sub_res = {'referer' => nil}
286
+ body = Nokogiri::XML::Builder.new do |xml|
287
+ xml.RefererConfiguration {
288
+ xml.AllowEmptyReferer referer.allow_empty?
289
+ xml.RefererList {
290
+ (referer.whitelist or []).each do |r|
291
+ xml.Referer r
292
+ end
293
+ }
294
+ }
295
+ end.to_xml
296
+
297
+ @http.put(
298
+ {:bucket => name, :sub_res => sub_res},
299
+ {:body => body})
300
+
301
+ logger.info("Done put bucket referer")
302
+ end
303
+
304
+ # Get bucket referer
305
+ # @param name [String] the bucket name
306
+ # @return [BucketReferer] the bucket referer options
307
+ def get_bucket_referer(name)
308
+ logger.info("Begin get bucket referer, name: #{name}")
309
+
310
+ sub_res = {'referer' => nil}
311
+ r = @http.get({:bucket => name, :sub_res => sub_res})
312
+
313
+ doc = parse_xml(r.body)
314
+ opts = {
315
+ :allow_empty =>
316
+ get_node_text(doc.root, 'AllowEmptyReferer', &:to_bool),
317
+ :whitelist => doc.css("RefererList Referer").map(&:text)
318
+ }
319
+
320
+ logger.info("Done get bucket referer")
321
+
322
+ BucketReferer.new(opts)
323
+ end
324
+
325
+ # Put bucket lifecycle settings
326
+ # @param name [String] the bucket name
327
+ # @param rules [Array<OSS::LifeCycleRule>] the
328
+ # lifecycle rules
329
+ # @see OSS::LifeCycleRule
330
+ def put_bucket_lifecycle(name, rules)
331
+ logger.info("Begin put bucket lifecycle, name: #{name}, rules: "\
332
+ "#{rules.map { |r| r.to_s }}")
333
+
334
+ sub_res = {'lifecycle' => nil}
335
+ body = Nokogiri::XML::Builder.new do |xml|
336
+ xml.LifecycleConfiguration {
337
+ rules.each do |r|
338
+ xml.Rule {
339
+ xml.ID r.id if r.id
340
+ xml.Status r.enabled? ? 'Enabled' : 'Disabled'
341
+
342
+ xml.Prefix r.prefix
343
+ xml.Expiration {
344
+ if r.expiry.is_a?(Date)
345
+ xml.Date Time.utc(
346
+ r.expiry.year, r.expiry.month, r.expiry.day)
347
+ .iso8601.sub('Z', '.000Z')
348
+ elsif r.expiry.is_a?(Fixnum)
349
+ xml.Days r.expiry
350
+ else
351
+ fail ClientError, "Expiry must be a Date or Fixnum."
352
+ end
353
+ }
354
+ }
355
+ end
356
+ }
357
+ end.to_xml
358
+
359
+ @http.put(
360
+ {:bucket => name, :sub_res => sub_res},
361
+ {:body => body})
362
+
363
+ logger.info("Done put bucket lifecycle")
364
+ end
365
+
366
+ # Get bucket lifecycle settings
367
+ # @param name [String] the bucket name
368
+ # @return [Array<OSS::LifeCycleRule>] the
369
+ # lifecycle rules. See {OSS::LifeCycleRule}
370
+ def get_bucket_lifecycle(name)
371
+ logger.info("Begin get bucket lifecycle, name: #{name}")
372
+
373
+ sub_res = {'lifecycle' => nil}
374
+ r = @http.get({:bucket => name, :sub_res => sub_res})
375
+
376
+ doc = parse_xml(r.body)
377
+ rules = doc.css("Rule").map do |n|
378
+ days = n.at_css("Expiration Days")
379
+ date = n.at_css("Expiration Date")
380
+
381
+ if (days && date) || (!days && !date)
382
+ fail ClientError, "We can only have one of Date and Days for expiry."
383
+ end
384
+
385
+ LifeCycleRule.new(
386
+ :id => get_node_text(n, 'ID'),
387
+ :prefix => get_node_text(n, 'Prefix'),
388
+ :enable => get_node_text(n, 'Status') { |x| x == 'Enabled' },
389
+ :expiry => days ? days.text.to_i : Date.parse(date.text)
390
+ )
391
+ end
392
+ logger.info("Done get bucket lifecycle")
393
+
394
+ rules
395
+ end
396
+
397
+ # Delete *all* lifecycle rules on the bucket
398
+ # @note this will delete all lifecycle rules
399
+ # @param name [String] the bucket name
400
+ def delete_bucket_lifecycle(name)
401
+ logger.info("Begin delete bucket lifecycle, name: #{name}")
402
+
403
+ sub_res = {'lifecycle' => nil}
404
+ @http.delete({:bucket => name, :sub_res => sub_res})
405
+
406
+ logger.info("Done delete bucket lifecycle")
407
+ end
408
+
409
+ # Set bucket CORS(Cross-Origin Resource Sharing) rules
410
+ # @param name [String] the bucket name
411
+ # @param rules [Array<OSS::CORSRule] the CORS
412
+ # rules
413
+ # @see OSS::CORSRule
414
+ def set_bucket_cors(name, rules)
415
+ logger.info("Begin set bucket cors, bucket: #{name}, rules: "\
416
+ "#{rules.map { |r| r.to_s }.join(';')}")
417
+
418
+ sub_res = {'cors' => nil}
419
+ body = Nokogiri::XML::Builder.new do |xml|
420
+ xml.CORSConfiguration {
421
+ rules.each do |r|
422
+ xml.CORSRule {
423
+ r.allowed_origins.each { |x| xml.AllowedOrigin x }
424
+ r.allowed_methods.each { |x| xml.AllowedMethod x }
425
+ r.allowed_headers.each { |x| xml.AllowedHeader x }
426
+ r.expose_headers.each { |x| xml.ExposeHeader x }
427
+ xml.MaxAgeSeconds r.max_age_seconds if r.max_age_seconds
428
+ }
429
+ end
430
+ }
431
+ end.to_xml
432
+
433
+ @http.put(
434
+ {:bucket => name, :sub_res => sub_res},
435
+ {:body => body})
436
+
437
+ logger.info("Done delete bucket lifecycle")
438
+ end
439
+
440
+ # Get bucket CORS rules
441
+ # @param name [String] the bucket name
442
+ # @return [Array<OSS::CORSRule] the CORS rules
443
+ def get_bucket_cors(name)
444
+ logger.info("Begin get bucket cors, bucket: #{name}")
445
+
446
+ sub_res = {'cors' => nil}
447
+ r = @http.get({:bucket => name, :sub_res => sub_res})
448
+
449
+ doc = parse_xml(r.body)
450
+ rules = []
451
+
452
+ doc.css("CORSRule").map do |n|
453
+ allowed_origins = n.css("AllowedOrigin").map(&:text)
454
+ allowed_methods = n.css("AllowedMethod").map(&:text)
455
+ allowed_headers = n.css("AllowedHeader").map(&:text)
456
+ expose_headers = n.css("ExposeHeader").map(&:text)
457
+ max_age_seconds = get_node_text(n, 'MaxAgeSeconds', &:to_i)
458
+
459
+ rules << CORSRule.new(
460
+ :allowed_origins => allowed_origins,
461
+ :allowed_methods => allowed_methods,
462
+ :allowed_headers => allowed_headers,
463
+ :expose_headers => expose_headers,
464
+ :max_age_seconds => max_age_seconds)
465
+ end
466
+
467
+ logger.info("Done get bucket cors")
468
+
469
+ rules
470
+ end
471
+
472
+ # Delete all bucket CORS rules
473
+ # @note this will delete all CORS rules of this bucket
474
+ # @param name [String] the bucket name
475
+ def delete_bucket_cors(name)
476
+ logger.info("Begin delete bucket cors, bucket: #{name}")
477
+
478
+ sub_res = {'cors' => nil}
479
+
480
+ @http.delete({:bucket => name, :sub_res => sub_res})
481
+
482
+ logger.info("Done delete bucket cors")
483
+ end
484
+
485
+ # Delete a bucket
486
+ # @param name [String] the bucket name
487
+ # @note it will fails if the bucket is not empty (it contains
488
+ # objects)
489
+ def delete_bucket(name)
490
+ logger.info("Begin delete bucket: #{name}")
491
+
492
+ @http.delete({:bucket => name})
493
+
494
+ logger.info("Done delete bucket")
495
+ end
496
+
497
+ # Put an object to the specified bucket, a block is required
498
+ # to provide the object data.
499
+ # @param bucket_name [String] the bucket name
500
+ # @param object_name [String] the object name
501
+ # @param opts [Hash] Options
502
+ # @option opts [String] :acl specify the object's ACL. See
503
+ # {OSS::ACL}
504
+ # @option opts [String] :content_type the HTTP Content-Type
505
+ # for the file, if not specified client will try to determine
506
+ # the type itself and fall back to HTTP::DEFAULT_CONTENT_TYPE
507
+ # if it fails to do so
508
+ # @option opts [Hash<Symbol, String>] :metas key-value pairs
509
+ # that serve as the object meta which will be stored together
510
+ # with the object
511
+ # @option opts [Callback] :callback the HTTP callback performed
512
+ # by OSS after `put_object` succeeds
513
+ # @option opts [Hash] :headers custom HTTP headers, case
514
+ # insensitive. Headers specified here will overwrite `:metas`
515
+ # and `:content_type`
516
+ # @yield [HTTP::StreamWriter] a stream writer is
517
+ # yielded to the caller to which it can write chunks of data
518
+ # streamingly
519
+ # @example
520
+ # chunk = get_chunk
521
+ # put_object('bucket', 'object') { |sw| sw.write(chunk) }
522
+ def put_object(bucket_name, object_name, opts = {}, &block)
523
+ logger.debug("Begin put object, bucket: #{bucket_name}, object: "\
524
+ "#{object_name}, options: #{opts}")
525
+
526
+ headers = {'content-type' => opts[:content_type]}
527
+ headers['x-oss-object-acl'] = opts[:acl] if opts.key?(:acl)
528
+ to_lower_case(opts[:metas] || {})
529
+ .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s }
530
+
531
+ headers.merge!(to_lower_case(opts[:headers])) if opts.key?(:headers)
532
+
533
+ if opts.key?(:callback)
534
+ headers[CALLBACK_HEADER] = opts[:callback].serialize
535
+ end
536
+
537
+ r = @http.put(
538
+ {:bucket => bucket_name, :object => object_name},
539
+ {:headers => headers, :body => HTTP::StreamPayload.new(&block)})
540
+
541
+ if r.code == 203
542
+ e = CallbackError.new(r)
543
+ logger.error(e.to_s)
544
+ raise e
545
+ end
546
+
547
+ logger.debug('Done put object')
548
+ end
549
+
550
+ # Append to an object of a bucket. Create an "Appendable
551
+ # Object" if the object does not exist. A block is required to
552
+ # provide the appending data.
553
+ # @param bucket_name [String] the bucket name
554
+ # @param object_name [String] the object name
555
+ # @param position [Integer] the position to append
556
+ # @param opts [Hash] Options
557
+ # @option opts [String] :acl specify the object's ACL. See
558
+ # {OSS::ACL}
559
+ # @option opts [String] :content_type the HTTP Content-Type
560
+ # for the file, if not specified client will try to determine
561
+ # the type itself and fall back to HTTP::DEFAULT_CONTENT_TYPE
562
+ # if it fails to do so
563
+ # @option opts [Hash<Symbol, String>] :metas key-value pairs
564
+ # that serve as the object meta which will be stored together
565
+ # with the object
566
+ # @option opts [Hash] :headers custom HTTP headers, case
567
+ # insensitive. Headers specified here will overwrite `:metas`
568
+ # and `:content_type`
569
+ # @return [Integer] next position to append
570
+ # @yield [HTTP::StreamWriter] a stream writer is
571
+ # yielded to the caller to which it can write chunks of data
572
+ # streamingly
573
+ # @note
574
+ # 1. Can not append to a "Normal Object"
575
+ # 2. The position must equal to the object's size before append
576
+ # 3. The :content_type is only used when the object is created
577
+ def append_object(bucket_name, object_name, position, opts = {}, &block)
578
+ logger.debug("Begin append object, bucket: #{bucket_name}, object: "\
579
+ "#{object_name}, position: #{position}, options: #{opts}")
580
+
581
+ sub_res = {'append' => nil, 'position' => position}
582
+ headers = {'content-type' => opts[:content_type]}
583
+ headers['x-oss-object-acl'] = opts[:acl] if opts.key?(:acl)
584
+ to_lower_case(opts[:metas] || {})
585
+ .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s }
586
+
587
+ headers.merge!(to_lower_case(opts[:headers])) if opts.key?(:headers)
588
+
589
+ r = @http.post(
590
+ {:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
591
+ {:headers => headers, :body => HTTP::StreamPayload.new(&block)})
592
+
593
+ logger.debug('Done append object')
594
+
595
+ wrap(r.headers[:x_oss_next_append_position], &:to_i) || -1
596
+ end
597
+
598
+ # List objects in a bucket.
599
+ # @param bucket_name [String] the bucket name
600
+ # @param opts [Hash] options
601
+ # @option opts [String] :prefix return only those buckets
602
+ # prefixed with it if specified
603
+ # @option opts [String] :marker return buckets after where it
604
+ # indicates (exclusively). All buckets are sorted by name
605
+ # alphabetically
606
+ # @option opts [Integer] :limit return only the first N
607
+ # buckets if specified
608
+ # @option opts [String] :delimiter the delimiter to get common
609
+ # prefixes of all objects
610
+ # @option opts [String] :encoding the encoding of object key
611
+ # in the response body. Only {OSS::KeyEncoding::URL} is
612
+ # supported now.
613
+ # @example
614
+ # Assume we have the following objects:
615
+ # /foo/bar/obj1
616
+ # /foo/bar/obj2
617
+ # ...
618
+ # /foo/bar/obj9999999
619
+ # /foo/xxx/
620
+ # use 'foo/' as the prefix, '/' as the delimiter, the common
621
+ # prefixes we get are: '/foo/bar/', '/foo/xxx/'. They are
622
+ # coincidentally the sub-directories under '/foo/'. Using
623
+ # delimiter we avoid list all the objects whose number may be
624
+ # large.
625
+ # @return [Array<Objects>, Hash] the returned object and a
626
+ # hash including the next tokens, which includes:
627
+ # * :common_prefixes [String] the common prefixes returned
628
+ # * :prefix [String] the prefix used
629
+ # * :delimiter [String] the delimiter used
630
+ # * :marker [String] the marker used
631
+ # * :limit [Integer] the limit used
632
+ # * :next_marker [String] marker to continue list objects
633
+ # * :truncated [Boolean] whether there are more objects to
634
+ # be returned
635
+ def list_objects(bucket_name, opts = {})
636
+ logger.debug("Begin list object, bucket: #{bucket_name}, options: #{opts}")
637
+
638
+ params = {
639
+ 'prefix' => opts[:prefix],
640
+ 'delimiter' => opts[:delimiter],
641
+ 'marker' => opts[:marker],
642
+ 'max-keys' => opts[:limit],
643
+ 'encoding-type' => opts[:encoding]
644
+ }.reject { |_, v| v.nil? }
645
+
646
+ r = @http.get({:bucket => bucket_name}, {:query => params})
647
+
648
+ doc = parse_xml(r.body)
649
+ encoding = get_node_text(doc.root, 'EncodingType')
650
+ objects = doc.css("Contents").map do |node|
651
+ Object.new(
652
+ :key => get_node_text(node, "Key") { |x| decode_key(x, encoding) },
653
+ :type => get_node_text(node, "Type"),
654
+ :size => get_node_text(node, "Size", &:to_i),
655
+ :etag => get_node_text(node, "ETag"),
656
+ :last_modified =>
657
+ get_node_text(node, "LastModified") { |x| Time.parse(x) }
658
+ )
659
+ end || []
660
+
661
+ more = {
662
+ :prefix => 'Prefix',
663
+ :delimiter => 'Delimiter',
664
+ :limit => 'MaxKeys',
665
+ :marker => 'Marker',
666
+ :next_marker => 'NextMarker',
667
+ :truncated => 'IsTruncated',
668
+ :encoding => 'EncodingType'
669
+ }.reduce({}) { |h, (k, v)|
670
+ value = get_node_text(doc.root, v)
671
+ value.nil?? h : h.merge(k => value)
672
+ }
673
+
674
+ update_if_exists(
675
+ more, {
676
+ :limit => ->(x) { x.to_i },
677
+ :truncated => ->(x) { x.to_bool },
678
+ :delimiter => ->(x) { decode_key(x, encoding) },
679
+ :marker => ->(x) { decode_key(x, encoding) },
680
+ :next_marker => ->(x) { decode_key(x, encoding) }
681
+ }
682
+ )
683
+
684
+ common_prefixes = []
685
+ doc.css("CommonPrefixes Prefix").map do |node|
686
+ common_prefixes << decode_key(node.text, encoding)
687
+ end
688
+ more[:common_prefixes] = common_prefixes unless common_prefixes.empty?
689
+
690
+ logger.debug("Done list object. objects: #{objects}, more: #{more}")
691
+
692
+ [objects, more]
693
+ end
694
+
695
+ # Get an object from the bucket. A block is required to handle
696
+ # the object data chunks.
697
+ # @note User can get the whole object or only part of it by specify
698
+ # the bytes range;
699
+ # @note User can specify conditions to get the object like:
700
+ # if-modified-since, if-unmodified-since, if-match-etag,
701
+ # if-unmatch-etag. If the object to get fails to meet the
702
+ # conditions, it will not be returned;
703
+ # @note User can indicate the server to rewrite the response headers
704
+ # such as content-type, content-encoding when get the object
705
+ # by specify the :rewrite options. The specified headers will
706
+ # be returned instead of the original property of the object.
707
+ # @param bucket_name [String] the bucket name
708
+ # @param object_name [String] the object name
709
+ # @param opts [Hash] options
710
+ # @option opts [Array<Integer>] :range bytes range to get from
711
+ # the object, in the format: xx-yy
712
+ # @option opts [Hash] :condition preconditions to get the object
713
+ # * :if_modified_since (Time) get the object if its modified
714
+ # time is later than specified
715
+ # * :if_unmodified_since (Time) get the object if its
716
+ # unmodified time if earlier than specified
717
+ # * :if_match_etag (String) get the object if its etag match
718
+ # specified
719
+ # * :if_unmatch_etag (String) get the object if its etag
720
+ # doesn't match specified
721
+ # @option opts [Hash] :headers custom HTTP headers, case
722
+ # insensitive. Headers specified here will overwrite `:condition`
723
+ # and `:range`
724
+ # @option opts [Hash] :rewrite response headers to rewrite
725
+ # * :content_type (String) the Content-Type header
726
+ # * :content_language (String) the Content-Language header
727
+ # * :expires (Time) the Expires header
728
+ # * :cache_control (String) the Cache-Control header
729
+ # * :content_disposition (String) the Content-Disposition header
730
+ # * :content_encoding (String) the Content-Encoding header
731
+ # @return [OSS::Object] The object meta
732
+ # @yield [String] it gives the data chunks of the object to
733
+ # the block
734
+ def get_object(bucket_name, object_name, opts = {}, &block)
735
+ logger.debug("Begin get object, bucket: #{bucket_name}, "\
736
+ "object: #{object_name}")
737
+
738
+ range = opts[:range]
739
+ conditions = opts[:condition]
740
+ rewrites = opts[:rewrite]
741
+
742
+ headers = {}
743
+ headers['range'] = get_bytes_range(range) if range
744
+ headers.merge!(get_conditions(conditions)) if conditions
745
+ headers.merge!(to_lower_case(opts[:headers])) if opts.key?(:headers)
746
+
747
+ sub_res = {}
748
+ if rewrites
749
+ [ :content_type,
750
+ :content_language,
751
+ :cache_control,
752
+ :content_disposition,
753
+ :content_encoding
754
+ ].each do |k|
755
+ key = "response-#{k.to_s.sub('_', '-')}"
756
+ sub_res[key] = rewrites[k] if rewrites.key?(k)
757
+ end
758
+ sub_res["response-expires"] =
759
+ rewrites[:expires].httpdate if rewrites.key?(:expires)
760
+ end
761
+
762
+ r = @http.get(
763
+ {:bucket => bucket_name, :object => object_name,
764
+ :sub_res => sub_res},
765
+ {:headers => headers}
766
+ ) { |chunk| yield chunk if block_given? }
767
+
768
+ h = r.headers
769
+ metas = {}
770
+ meta_prefix = 'x_oss_meta_'
771
+ h.select { |k, _| k.to_s.start_with?(meta_prefix) }
772
+ .each { |k, v| metas[k.to_s.sub(meta_prefix, '')] = v.to_s }
773
+
774
+ obj = Object.new(
775
+ :key => object_name,
776
+ :type => h[:x_oss_object_type],
777
+ :size => wrap(h[:content_length], &:to_i),
778
+ :etag => h[:etag],
779
+ :metas => metas,
780
+ :last_modified => wrap(h[:last_modified]) { |x| Time.parse(x) },
781
+ :headers => h)
782
+
783
+ logger.debug("Done get object")
784
+
785
+ obj
786
+ end
787
+
788
+ # Get the object meta rather than the whole object.
789
+ # @note User can specify conditions to get the object like:
790
+ # if-modified-since, if-unmodified-since, if-match-etag,
791
+ # if-unmatch-etag. If the object to get fails to meet the
792
+ # conditions, it will not be returned.
793
+ #
794
+ # @param bucket_name [String] the bucket name
795
+ # @param object_name [String] the object name
796
+ # @param opts [Hash] options
797
+ # @option opts [Hash] :condition preconditions to get the
798
+ # object meta. The same as #get_object
799
+ # @return [OSS::Object] The object meta
800
+ def get_object_meta(bucket_name, object_name, opts = {})
801
+ logger.debug("Begin get object meta, bucket: #{bucket_name}, "\
802
+ "object: #{object_name}, options: #{opts}")
803
+
804
+ headers = {}
805
+ headers.merge!(get_conditions(opts[:condition])) if opts[:condition]
806
+
807
+ r = @http.head(
808
+ {:bucket => bucket_name, :object => object_name},
809
+ {:headers => headers})
810
+
811
+ h = r.headers
812
+ metas = {}
813
+ meta_prefix = 'x_oss_meta_'
814
+ h.select { |k, _| k.to_s.start_with?(meta_prefix) }
815
+ .each { |k, v| metas[k.to_s.sub(meta_prefix, '')] = v.to_s }
816
+
817
+ obj = Object.new(
818
+ :key => object_name,
819
+ :type => h[:x_oss_object_type],
820
+ :size => wrap(h[:content_length], &:to_i),
821
+ :etag => h[:etag],
822
+ :metas => metas,
823
+ :last_modified => wrap(h[:last_modified]) { |x| Time.parse(x) },
824
+ :headers => h)
825
+
826
+ logger.debug("Done get object meta")
827
+
828
+ obj
829
+ end
830
+
831
+ # Copy an object in the bucket. The source object and the dest
832
+ # object may be from different buckets of the same region.
833
+ # @param bucket_name [String] the bucket name
834
+ # @param src_object_name [String] the source object name
835
+ # @param dst_object_name [String] the dest object name
836
+ # @param opts [Hash] options
837
+ # @option opts [String] :src_bucket specify the source object's
838
+ # bucket. It MUST be in the same region as the dest bucket. It
839
+ # defaults to dest bucket if not specified.
840
+ # @option opts [String] :acl specify the dest object's
841
+ # ACL. See {OSS::ACL}
842
+ # @option opts [String] :meta_directive specify what to do
843
+ # with the object's meta: copy or replace. See
844
+ # {OSS::MetaDirective}
845
+ # @option opts [String] :content_type the HTTP Content-Type
846
+ # for the file, if not specified client will try to determine
847
+ # the type itself and fall back to HTTP::DEFAULT_CONTENT_TYPE
848
+ # if it fails to do so
849
+ # @option opts [Hash<Symbol, String>] :metas key-value pairs
850
+ # that serve as the object meta which will be stored together
851
+ # with the object
852
+ # @option opts [Hash] :condition preconditions to get the
853
+ # object. See #get_object
854
+ # @return [Hash] the copy result
855
+ # * :etag [String] the etag of the dest object
856
+ # * :last_modified [Time] the last modification time of the
857
+ # dest object
858
+ def copy_object(bucket_name, src_object_name, dst_object_name, opts = {})
859
+ logger.debug("Begin copy object, bucket: #{bucket_name}, "\
860
+ "source object: #{src_object_name}, dest object: "\
861
+ "#{dst_object_name}, options: #{opts}")
862
+
863
+ src_bucket = opts[:src_bucket] || bucket_name
864
+ headers = {
865
+ 'x-oss-copy-source' =>
866
+ @http.get_resource_path(src_bucket, src_object_name),
867
+ 'content-type' => opts[:content_type]
868
+ }
869
+ (opts[:metas] || {})
870
+ .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s }
871
+
872
+ {
873
+ :acl => 'x-oss-object-acl',
874
+ :meta_directive => 'x-oss-metadata-directive'
875
+ }.each { |k, v| headers[v] = opts[k] if opts[k] }
876
+
877
+ headers.merge!(get_copy_conditions(opts[:condition])) if opts[:condition]
878
+
879
+ r = @http.put(
880
+ {:bucket => bucket_name, :object => dst_object_name},
881
+ {:headers => headers})
882
+
883
+ doc = parse_xml(r.body)
884
+ copy_result = {
885
+ :last_modified => get_node_text(
886
+ doc.root, 'LastModified') { |x| Time.parse(x) },
887
+ :etag => get_node_text(doc.root, 'ETag')
888
+ }.reject { |_, v| v.nil? }
889
+
890
+ logger.debug("Done copy object")
891
+
892
+ copy_result
893
+ end
894
+
895
+ # Delete an object from the bucket
896
+ # @param bucket_name [String] the bucket name
897
+ # @param object_name [String] the object name
898
+ def delete_object(bucket_name, object_name)
899
+ logger.debug("Begin delete object, bucket: #{bucket_name}, "\
900
+ "object: #{object_name}")
901
+
902
+ @http.delete({:bucket => bucket_name, :object => object_name})
903
+
904
+ logger.debug("Done delete object")
905
+ end
906
+
907
+ # Batch delete objects
908
+ # @param bucket_name [String] the bucket name
909
+ # @param object_names [Enumerator<String>] the object names
910
+ # @param opts [Hash] options
911
+ # @option opts [Boolean] :quiet indicates whether the server
912
+ # should return the delete result of the objects
913
+ # @option opts [String] :encoding the encoding type for
914
+ # object key in the response body, only
915
+ # {OSS::KeyEncoding::URL} is supported now
916
+ # @return [Array<String>] object names that have been
917
+ # successfully deleted or empty if :quiet is true
918
+ def batch_delete_objects(bucket_name, object_names, opts = {})
919
+ logger.debug("Begin batch delete object, bucket: #{bucket_name}, "\
920
+ "objects: #{object_names}, options: #{opts}")
921
+
922
+ sub_res = {'delete' => nil}
923
+
924
+ # It may have invisible chars in object key which will corrupt
925
+ # libxml. So we're constructing xml body manually here.
926
+ body = '<?xml version="1.0"?>'
927
+ body << '<Delete>'
928
+ body << '<Quiet>' << (opts[:quiet]? true : false).to_s << '</Quiet>'
929
+ object_names.each { |k|
930
+ body << '<Object><Key>' << CGI.escapeHTML(k) << '</Key></Object>'
931
+ }
932
+ body << '</Delete>'
933
+
934
+ query = {}
935
+ query['encoding-type'] = opts[:encoding] if opts[:encoding]
936
+
937
+ r = @http.post(
938
+ {:bucket => bucket_name, :sub_res => sub_res},
939
+ {:query => query, :body => body})
940
+
941
+ deleted = []
942
+ unless opts[:quiet]
943
+ doc = parse_xml(r.body)
944
+ encoding = get_node_text(doc.root, 'EncodingType')
945
+ doc.css("Deleted").map do |n|
946
+ deleted << get_node_text(n, 'Key') { |x| decode_key(x, encoding) }
947
+ end
948
+ end
949
+
950
+ logger.debug("Done delete object")
951
+
952
+ deleted
953
+ end
954
+
955
+ # Put object acl
956
+ # @param bucket_name [String] the bucket name
957
+ # @param object_name [String] the object name
958
+ # @param acl [String] the object's ACL. See {OSS::ACL}
959
+ def put_object_acl(bucket_name, object_name, acl)
960
+ logger.debug("Begin update object acl, bucket: #{bucket_name}, "\
961
+ "object: #{object_name}, acl: #{acl}")
962
+
963
+ sub_res = {'acl' => nil}
964
+ headers = {'x-oss-object-acl' => acl}
965
+
966
+ @http.put(
967
+ {:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
968
+ {:headers => headers})
969
+
970
+ logger.debug("Done update object acl")
971
+ end
972
+
973
+ # Get object acl
974
+ # @param bucket_name [String] the bucket name
975
+ # @param object_name [String] the object name
976
+ # [return] the object's acl. See {OSS::ACL}
977
+ def get_object_acl(bucket_name, object_name)
978
+ logger.debug("Begin get object acl, bucket: #{bucket_name}, "\
979
+ "object: #{object_name}")
980
+
981
+ sub_res = {'acl' => nil}
982
+ r = @http.get(
983
+ {bucket: bucket_name, object: object_name, sub_res: sub_res})
984
+
985
+ doc = parse_xml(r.body)
986
+ acl = get_node_text(doc.at_css("AccessControlList"), 'Grant')
987
+
988
+ logger.debug("Done get object acl")
989
+
990
+ acl
991
+ end
992
+
993
+ # Get object CORS rule
994
+ # @note this is usually used by browser to make a "preflight"
995
+ # @param bucket_name [String] the bucket name
996
+ # @param object_name [String] the object name
997
+ # @param origin [String] the Origin of the reqeust
998
+ # @param method [String] the method to request access:
999
+ # Access-Control-Request-Method
1000
+ # @param headers [Array<String>] the headers to request access:
1001
+ # Access-Control-Request-Headers
1002
+ # @return [CORSRule] the CORS rule of the object
1003
+ def get_object_cors(bucket_name, object_name, origin, method, headers = [])
1004
+ logger.debug("Begin get object cors, bucket: #{bucket_name}, object: "\
1005
+ "#{object_name}, origin: #{origin}, method: #{method}, "\
1006
+ "headers: #{headers.join(',')}")
1007
+
1008
+ h = {
1009
+ 'origin' => origin,
1010
+ 'access-control-request-method' => method,
1011
+ 'access-control-request-headers' => headers.join(',')
1012
+ }
1013
+
1014
+ r = @http.options(
1015
+ {:bucket => bucket_name, :object => object_name},
1016
+ {:headers => h})
1017
+
1018
+ logger.debug("Done get object cors")
1019
+
1020
+ CORSRule.new(
1021
+ :allowed_origins => r.headers[:access_control_allow_origin],
1022
+ :allowed_methods => r.headers[:access_control_allow_methods],
1023
+ :allowed_headers => r.headers[:access_control_allow_headers],
1024
+ :expose_headers => r.headers[:access_control_expose_headers],
1025
+ :max_age_seconds => r.headers[:access_control_max_age]
1026
+ )
1027
+ end
1028
+
1029
+ ##
1030
+ # Multipart uploading
1031
+ #
1032
+
1033
+ # Initiate a a multipart uploading transaction
1034
+ # @param bucket_name [String] the bucket name
1035
+ # @param object_name [String] the object name
1036
+ # @param opts [Hash] options
1037
+ # @option opts [String] :content_type the HTTP Content-Type
1038
+ # for the file, if not specified client will try to determine
1039
+ # the type itself and fall back to HTTP::DEFAULT_CONTENT_TYPE
1040
+ # if it fails to do so
1041
+ # @option opts [Hash<Symbol, String>] :metas key-value pairs
1042
+ # that serve as the object meta which will be stored together
1043
+ # with the object
1044
+ # @option opts [Hash] :headers custom HTTP headers, case
1045
+ # insensitive. Headers specified here will overwrite `:metas`
1046
+ # and `:content_type`
1047
+ # @return [String] the upload id
1048
+ def initiate_multipart_upload(bucket_name, object_name, opts = {})
1049
+ logger.info("Begin initiate multipart upload, bucket: "\
1050
+ "#{bucket_name}, object: #{object_name}, options: #{opts}")
1051
+
1052
+ sub_res = {'uploads' => nil}
1053
+ headers = {'content-type' => opts[:content_type]}
1054
+ to_lower_case(opts[:metas] || {})
1055
+ .each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s }
1056
+
1057
+ headers.merge!(to_lower_case(opts[:headers])) if opts.key?(:headers)
1058
+
1059
+ r = @http.post(
1060
+ {:bucket => bucket_name, :object => object_name,
1061
+ :sub_res => sub_res},
1062
+ {:headers => headers})
1063
+
1064
+ doc = parse_xml(r.body)
1065
+ txn_id = get_node_text(doc.root, 'UploadId')
1066
+
1067
+ logger.info("Done initiate multipart upload: #{txn_id}.")
1068
+
1069
+ txn_id
1070
+ end
1071
+
1072
+ # Upload a part in a multipart uploading transaction.
1073
+ # @param bucket_name [String] the bucket name
1074
+ # @param object_name [String] the object name
1075
+ # @param txn_id [String] the upload id
1076
+ # @param part_no [Integer] the part number
1077
+ # @yield [HTTP::StreamWriter] a stream writer is
1078
+ # yielded to the caller to which it can write chunks of data
1079
+ # streamingly
1080
+ def upload_part(bucket_name, object_name, txn_id, part_no, &block)
1081
+ logger.debug("Begin upload part, bucket: #{bucket_name}, object: "\
1082
+ "#{object_name}, txn id: #{txn_id}, part No: #{part_no}")
1083
+
1084
+ sub_res = {'partNumber' => part_no, 'uploadId' => txn_id}
1085
+ r = @http.put(
1086
+ {:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
1087
+ {:body => HTTP::StreamPayload.new(&block)})
1088
+
1089
+ logger.debug("Done upload part")
1090
+
1091
+ Multipart::Part.new(:number => part_no, :etag => r.headers[:etag])
1092
+ end
1093
+
1094
+ # Upload a part in a multipart uploading transaction by copying
1095
+ # from an existent object as the part's content. It may copy
1096
+ # only part of the object by specifying the bytes range to read.
1097
+ # @param bucket_name [String] the bucket name
1098
+ # @param object_name [String] the object name
1099
+ # @param txn_id [String] the upload id
1100
+ # @param part_no [Integer] the part number
1101
+ # @param source_object [String] the source object name to copy from
1102
+ # @param opts [Hash] options
1103
+ # @option opts [String] :src_bucket specify the source object's
1104
+ # bucket. It MUST be in the same region as the dest bucket. It
1105
+ # defaults to dest bucket if not specified.
1106
+ # @option opts [Array<Integer>] :range the bytes range to
1107
+ # copy, int the format: [begin(inclusive), end(exclusive)]
1108
+ # @option opts [Hash] :condition preconditions to copy the
1109
+ # object. See #get_object
1110
+ def upload_part_by_copy(
1111
+ bucket_name, object_name, txn_id, part_no, source_object, opts = {})
1112
+ logger.debug("Begin upload part by copy, bucket: #{bucket_name}, "\
1113
+ "object: #{object_name}, source object: #{source_object}"\
1114
+ "txn id: #{txn_id}, part No: #{part_no}, options: #{opts}")
1115
+
1116
+ range = opts[:range]
1117
+ conditions = opts[:condition]
1118
+
1119
+ if range && (!range.is_a?(Array) || range.size != 2)
1120
+ fail ClientError, "Range must be an array containing 2 Integers."
1121
+ end
1122
+
1123
+ src_bucket = opts[:src_bucket] || bucket_name
1124
+ headers = {
1125
+ 'x-oss-copy-source' =>
1126
+ @http.get_resource_path(src_bucket, source_object)
1127
+ }
1128
+ headers['range'] = get_bytes_range(range) if range
1129
+ headers.merge!(get_copy_conditions(conditions)) if conditions
1130
+
1131
+ sub_res = {'partNumber' => part_no, 'uploadId' => txn_id}
1132
+
1133
+ r = @http.put(
1134
+ {:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
1135
+ {:headers => headers})
1136
+
1137
+ logger.debug("Done upload part by copy: #{source_object}.")
1138
+
1139
+ Multipart::Part.new(:number => part_no, :etag => r.headers[:etag])
1140
+ end
1141
+
1142
+ # Complete a multipart uploading transaction
1143
+ # @param bucket_name [String] the bucket name
1144
+ # @param object_name [String] the object name
1145
+ # @param txn_id [String] the upload id
1146
+ # @param parts [Array<Multipart::Part>] all the parts in this
1147
+ # transaction
1148
+ # @param callback [Callback] the HTTP callback performed by OSS
1149
+ # after this operation succeeds
1150
+ def complete_multipart_upload(
1151
+ bucket_name, object_name, txn_id, parts, callback = nil)
1152
+ logger.debug("Begin complete multipart upload, "\
1153
+ "txn id: #{txn_id}, parts: #{parts.map(&:to_s)}")
1154
+
1155
+ sub_res = {'uploadId' => txn_id}
1156
+ headers = {}
1157
+ headers[CALLBACK_HEADER] = callback.serialize if callback
1158
+
1159
+ body = Nokogiri::XML::Builder.new do |xml|
1160
+ xml.CompleteMultipartUpload {
1161
+ parts.each do |p|
1162
+ xml.Part {
1163
+ xml.PartNumber p.number
1164
+ xml.ETag p.etag
1165
+ }
1166
+ end
1167
+ }
1168
+ end.to_xml
1169
+
1170
+ r = @http.post(
1171
+ {:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
1172
+ {:headers => headers, :body => body})
1173
+
1174
+ if r.code == 203
1175
+ e = CallbackError.new(r)
1176
+ logger.error(e.to_s)
1177
+ raise e
1178
+ end
1179
+
1180
+ logger.debug("Done complete multipart upload: #{txn_id}.")
1181
+ end
1182
+
1183
+ # Abort a multipart uploading transaction
1184
+ # @note All the parts are discarded after abort. For some parts
1185
+ # being uploaded while the abort happens, they may not be
1186
+ # discarded. Call abort_multipart_upload several times for this
1187
+ # situation.
1188
+ # @param bucket_name [String] the bucket name
1189
+ # @param object_name [String] the object name
1190
+ # @param txn_id [String] the upload id
1191
+ def abort_multipart_upload(bucket_name, object_name, txn_id)
1192
+ logger.debug("Begin abort multipart upload, txn id: #{txn_id}")
1193
+
1194
+ sub_res = {'uploadId' => txn_id}
1195
+
1196
+ @http.delete(
1197
+ {:bucket => bucket_name, :object => object_name, :sub_res => sub_res})
1198
+
1199
+ logger.debug("Done abort multipart: #{txn_id}.")
1200
+ end
1201
+
1202
+ # Get a list of all the on-going multipart uploading
1203
+ # transactions. That is: thoses started and not aborted.
1204
+ # @param bucket_name [String] the bucket name
1205
+ # @param opts [Hash] options:
1206
+ # @option opts [String] :id_marker return only thoese
1207
+ # transactions with txn id after :id_marker
1208
+ # @option opts [String] :key_marker the object key marker for
1209
+ # a multipart upload transaction.
1210
+ # 1. if +:id_marker+ is not set, return only those
1211
+ # transactions with object key *after* +:key_marker+;
1212
+ # 2. if +:id_marker+ is set, return only thoese transactions
1213
+ # with object key *equals* +:key_marker+ and txn id after
1214
+ # +:id_marker+
1215
+ # @option opts [String] :prefix the prefix of the object key
1216
+ # for a multipart upload transaction. if set only return
1217
+ # those transactions with the object key prefixed with it
1218
+ # @option opts [String] :encoding the encoding of object key
1219
+ # in the response body. Only {OSS::KeyEncoding::URL} is
1220
+ # supported now.
1221
+ # @return [Array<Multipart::Transaction>, Hash]
1222
+ # the returned transactions and a hash including next tokens,
1223
+ # which includes:
1224
+ # * :prefix [String] the prefix used
1225
+ # * :limit [Integer] the limit used
1226
+ # * :id_marker [String] the upload id marker used
1227
+ # * :next_id_marker [String] upload id marker to continue list
1228
+ # multipart transactions
1229
+ # * :key_marker [String] the object key marker used
1230
+ # * :next_key_marker [String] object key marker to continue
1231
+ # list multipart transactions
1232
+ # * :truncated [Boolean] whether there are more transactions
1233
+ # to be returned
1234
+ # * :encoding [String] the object key encoding used
1235
+ def list_multipart_uploads(bucket_name, opts = {})
1236
+ logger.debug("Begin list multipart uploads, "\
1237
+ "bucket: #{bucket_name}, opts: #{opts}")
1238
+
1239
+ sub_res = {'uploads' => nil}
1240
+ params = {
1241
+ 'prefix' => opts[:prefix],
1242
+ 'upload-id-marker' => opts[:id_marker],
1243
+ 'key-marker' => opts[:key_marker],
1244
+ 'max-uploads' => opts[:limit],
1245
+ 'encoding-type' => opts[:encoding]
1246
+ }.reject { |_, v| v.nil? }
1247
+
1248
+ r = @http.get(
1249
+ {:bucket => bucket_name, :sub_res => sub_res},
1250
+ {:query => params})
1251
+
1252
+ doc = parse_xml(r.body)
1253
+ encoding = get_node_text(doc.root, 'EncodingType')
1254
+ txns = doc.css("Upload").map do |node|
1255
+ Multipart::Transaction.new(
1256
+ :id => get_node_text(node, "UploadId"),
1257
+ :object => get_node_text(node, "Key") { |x| decode_key(x, encoding) },
1258
+ :bucket => bucket_name,
1259
+ :creation_time =>
1260
+ get_node_text(node, "Initiated") { |t| Time.parse(t) }
1261
+ )
1262
+ end || []
1263
+
1264
+ more = {
1265
+ :prefix => 'Prefix',
1266
+ :limit => 'MaxUploads',
1267
+ :id_marker => 'UploadIdMarker',
1268
+ :next_id_marker => 'NextUploadIdMarker',
1269
+ :key_marker => 'KeyMarker',
1270
+ :next_key_marker => 'NextKeyMarker',
1271
+ :truncated => 'IsTruncated',
1272
+ :encoding => 'EncodingType'
1273
+ }.reduce({}) { |h, (k, v)|
1274
+ value = get_node_text(doc.root, v)
1275
+ value.nil?? h : h.merge(k => value)
1276
+ }
1277
+
1278
+ update_if_exists(
1279
+ more, {
1280
+ :limit => ->(x) { x.to_i },
1281
+ :truncated => ->(x) { x.to_bool },
1282
+ :key_marker => ->(x) { decode_key(x, encoding) },
1283
+ :next_key_marker => ->(x) { decode_key(x, encoding) }
1284
+ }
1285
+ )
1286
+
1287
+ logger.debug("Done list multipart transactions")
1288
+
1289
+ [txns, more]
1290
+ end
1291
+
1292
+ # Get a list of parts that are successfully uploaded in a
1293
+ # transaction.
1294
+ # @param txn_id [String] the upload id
1295
+ # @param opts [Hash] options:
1296
+ # @option opts [Integer] :marker the part number marker after
1297
+ # which to return parts
1298
+ # @option opts [Integer] :limit max number parts to return
1299
+ # @return [Array<Multipart::Part>, Hash] the returned parts and
1300
+ # a hash including next tokens, which includes:
1301
+ # * :marker [Integer] the marker used
1302
+ # * :limit [Integer] the limit used
1303
+ # * :next_marker [Integer] marker to continue list parts
1304
+ # * :truncated [Boolean] whether there are more parts to be
1305
+ # returned
1306
+ def list_parts(bucket_name, object_name, txn_id, opts = {})
1307
+ logger.debug("Begin list parts, bucket: #{bucket_name}, object: "\
1308
+ "#{object_name}, txn id: #{txn_id}, options: #{opts}")
1309
+
1310
+ sub_res = {'uploadId' => txn_id}
1311
+ params = {
1312
+ 'part-number-marker' => opts[:marker],
1313
+ 'max-parts' => opts[:limit],
1314
+ 'encoding-type' => opts[:encoding]
1315
+ }.reject { |_, v| v.nil? }
1316
+
1317
+ r = @http.get(
1318
+ {:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
1319
+ {:query => params})
1320
+
1321
+ doc = parse_xml(r.body)
1322
+ parts = doc.css("Part").map do |node|
1323
+ Multipart::Part.new(
1324
+ :number => get_node_text(node, 'PartNumber', &:to_i),
1325
+ :etag => get_node_text(node, 'ETag'),
1326
+ :size => get_node_text(node, 'Size', &:to_i),
1327
+ :last_modified =>
1328
+ get_node_text(node, 'LastModified') { |x| Time.parse(x) })
1329
+ end || []
1330
+
1331
+ more = {
1332
+ :limit => 'MaxParts',
1333
+ :marker => 'PartNumberMarker',
1334
+ :next_marker => 'NextPartNumberMarker',
1335
+ :truncated => 'IsTruncated',
1336
+ :encoding => 'EncodingType'
1337
+ }.reduce({}) { |h, (k, v)|
1338
+ value = get_node_text(doc.root, v)
1339
+ value.nil?? h : h.merge(k => value)
1340
+ }
1341
+
1342
+ update_if_exists(
1343
+ more, {
1344
+ :limit => ->(x) { x.to_i },
1345
+ :truncated => ->(x) { x.to_bool }
1346
+ }
1347
+ )
1348
+
1349
+ logger.debug("Done list parts, parts: #{parts}, more: #{more}")
1350
+
1351
+ [parts, more]
1352
+ end
1353
+
1354
+ # Get bucket/object url
1355
+ # @param [String] bucket the bucket name
1356
+ # @param [String] object the bucket name
1357
+ # @return [String] url for the bucket/object
1358
+ def get_request_url(bucket, object = nil)
1359
+ @http.get_request_url(bucket, object)
1360
+ end
1361
+
1362
+ # Get user's access key id
1363
+ # @return [String] the access key id
1364
+ def get_access_key_id
1365
+ @config.access_key_id
1366
+ end
1367
+
1368
+ # Get user's STS token
1369
+ # @return [String] the STS token
1370
+ def get_sts_token
1371
+ @config.sts_token
1372
+ end
1373
+
1374
+ # Sign a string using the stored access key secret
1375
+ # @param [String] string_to_sign the string to sign
1376
+ # @return [String] the signature
1377
+ def sign(string_to_sign)
1378
+ Util.sign(@config.access_key_secret, string_to_sign)
1379
+ end
1380
+
1381
+ private
1382
+
1383
+ # Parse body content to xml document
1384
+ # @param content [String] the xml content
1385
+ # @return [Nokogiri::XML::Document] the parsed document
1386
+ def parse_xml(content)
1387
+ doc = Nokogiri::XML(content) do |config|
1388
+ config.options |= Nokogiri::XML::ParseOptions::NOBLANKS
1389
+ end
1390
+
1391
+ doc
1392
+ end
1393
+
1394
+ # Get the text of a xml node
1395
+ # @param node [Nokogiri::XML::Node] the xml node
1396
+ # @param tag [String] the node tag
1397
+ # @yield [String] the node text is given to the block
1398
+ def get_node_text(node, tag, &block)
1399
+ n = node.at_css(tag) if node
1400
+ value = n.text if n
1401
+ block && value ? yield(value) : value
1402
+ end
1403
+
1404
+ # Decode object key using encoding. If encoding is nil it
1405
+ # returns the key directly.
1406
+ # @param key [String] the object key
1407
+ # @param encoding [String] the encoding used
1408
+ # @return [String] the decoded key
1409
+ def decode_key(key, encoding)
1410
+ return key unless encoding
1411
+
1412
+ unless KeyEncoding.include?(encoding)
1413
+ fail ClientError, "Unsupported key encoding: #{encoding}"
1414
+ end
1415
+
1416
+ if encoding == KeyEncoding::URL
1417
+ return CGI.unescape(key)
1418
+ end
1419
+ end
1420
+
1421
+ # Transform x if x is not nil
1422
+ # @param x [Object] the object to transform
1423
+ # @yield [Object] the object if given to the block
1424
+ # @return [Object] the transformed object
1425
+ def wrap(x, &block)
1426
+ yield x if x
1427
+ end
1428
+
1429
+ # Get conditions for HTTP headers
1430
+ # @param conditions [Hash] the conditions
1431
+ # @return [Hash] conditions for HTTP headers
1432
+ def get_conditions(conditions)
1433
+ {
1434
+ :if_modified_since => 'if-modified-since',
1435
+ :if_unmodified_since => 'if-unmodified-since',
1436
+ }.reduce({}) { |h, (k, v)|
1437
+ conditions.key?(k)? h.merge(v => conditions[k].httpdate) : h
1438
+ }.merge(
1439
+ {
1440
+ :if_match_etag => 'if-match',
1441
+ :if_unmatch_etag => 'if-none-match'
1442
+ }.reduce({}) { |h, (k, v)|
1443
+ conditions.key?(k)? h.merge(v => conditions[k]) : h
1444
+ }
1445
+ )
1446
+ end
1447
+
1448
+ # Get copy conditions for HTTP headers
1449
+ # @param conditions [Hash] the conditions
1450
+ # @return [Hash] copy conditions for HTTP headers
1451
+ def get_copy_conditions(conditions)
1452
+ {
1453
+ :if_modified_since => 'x-oss-copy-source-if-modified-since',
1454
+ :if_unmodified_since => 'x-oss-copy-source-if-unmodified-since',
1455
+ }.reduce({}) { |h, (k, v)|
1456
+ conditions.key?(k)? h.merge(v => conditions[k].httpdate) : h
1457
+ }.merge(
1458
+ {
1459
+ :if_match_etag => 'x-oss-copy-source-if-match',
1460
+ :if_unmatch_etag => 'x-oss-copy-source-if-none-match'
1461
+ }.reduce({}) { |h, (k, v)|
1462
+ conditions.key?(k)? h.merge(v => conditions[k]) : h
1463
+ }
1464
+ )
1465
+ end
1466
+
1467
+ # Get bytes range
1468
+ # @param range [Array<Integer>] range
1469
+ # @return [String] bytes range for HTTP headers
1470
+ def get_bytes_range(range)
1471
+ if range &&
1472
+ (!range.is_a?(Array) || range.size != 2 ||
1473
+ !range.at(0).is_a?(Fixnum) || !range.at(1).is_a?(Fixnum))
1474
+ fail ClientError, "Range must be an array containing 2 Integers."
1475
+ end
1476
+
1477
+ "bytes=#{range.at(0)}-#{range.at(1) - 1}"
1478
+ end
1479
+
1480
+ # Update values for keys that exist in hash
1481
+ # @param hash [Hash] the hash to be updated
1482
+ # @param kv [Hash] keys & blocks to updated
1483
+ def update_if_exists(hash, kv)
1484
+ kv.each { |k, v| hash[k] = v.call(hash[k]) if hash.key?(k) }
1485
+ end
1486
+
1487
+ # Convert hash keys to lower case Non-Recursively
1488
+ # @param hash [Hash] the hash to be converted
1489
+ # @return [Hash] hash with lower case keys
1490
+ def to_lower_case(hash)
1491
+ hash.reduce({}) do |result, (k, v)|
1492
+ result[k.to_s.downcase] = v
1493
+ result
1494
+ end
1495
+ end
1496
+
1497
+ end # Protocol
1498
+ end # OSS
1499
+ end # Aliyun