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