aliyun-sdk 0.4.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +174 -172
  4. data/examples/aliyun/oss/bucket.rb +0 -0
  5. data/examples/aliyun/oss/callback.rb +0 -0
  6. data/examples/aliyun/oss/object.rb +0 -0
  7. data/examples/aliyun/oss/resumable_download.rb +0 -0
  8. data/examples/aliyun/oss/resumable_upload.rb +0 -0
  9. data/examples/aliyun/oss/streaming.rb +0 -0
  10. data/examples/aliyun/oss/using_sts.rb +0 -0
  11. data/examples/aliyun/sts/assume_role.rb +0 -0
  12. data/ext/crcx/crc64_ecma.c +270 -0
  13. data/ext/crcx/crcx.c +45 -0
  14. data/ext/crcx/crcx.h +8 -0
  15. data/ext/crcx/extconf.rb +3 -0
  16. data/lib/aliyun/common.rb +0 -0
  17. data/lib/aliyun/common/exception.rb +0 -0
  18. data/lib/aliyun/common/logging.rb +6 -1
  19. data/lib/aliyun/common/struct.rb +0 -0
  20. data/lib/aliyun/oss.rb +1 -0
  21. data/lib/aliyun/oss/bucket.rb +41 -33
  22. data/lib/aliyun/oss/client.rb +10 -2
  23. data/lib/aliyun/oss/config.rb +4 -1
  24. data/lib/aliyun/oss/download.rb +2 -2
  25. data/lib/aliyun/oss/exception.rb +6 -0
  26. data/lib/aliyun/oss/http.rb +32 -48
  27. data/lib/aliyun/oss/iterator.rb +0 -0
  28. data/lib/aliyun/oss/multipart.rb +1 -1
  29. data/lib/aliyun/oss/object.rb +0 -0
  30. data/lib/aliyun/oss/protocol.rb +68 -8
  31. data/lib/aliyun/oss/struct.rb +2 -2
  32. data/lib/aliyun/oss/upload.rb +0 -0
  33. data/lib/aliyun/oss/util.rb +25 -1
  34. data/lib/aliyun/sts.rb +0 -0
  35. data/lib/aliyun/sts/client.rb +1 -1
  36. data/lib/aliyun/sts/config.rb +0 -0
  37. data/lib/aliyun/sts/exception.rb +0 -0
  38. data/lib/aliyun/sts/protocol.rb +3 -3
  39. data/lib/aliyun/sts/struct.rb +0 -0
  40. data/lib/aliyun/sts/util.rb +0 -0
  41. data/lib/aliyun/version.rb +1 -1
  42. data/spec/aliyun/oss/bucket_spec.rb +194 -18
  43. data/spec/aliyun/oss/client/bucket_spec.rb +342 -30
  44. data/spec/aliyun/oss/client/client_spec.rb +26 -1
  45. data/spec/aliyun/oss/client/resumable_download_spec.rb +0 -0
  46. data/spec/aliyun/oss/client/resumable_upload_spec.rb +0 -0
  47. data/spec/aliyun/oss/http_spec.rb +26 -0
  48. data/spec/aliyun/oss/multipart_spec.rb +53 -8
  49. data/spec/aliyun/oss/object_spec.rb +256 -10
  50. data/spec/aliyun/oss/service_spec.rb +0 -0
  51. data/spec/aliyun/oss/util_spec.rb +101 -0
  52. data/spec/aliyun/sts/client_spec.rb +0 -0
  53. data/spec/aliyun/sts/util_spec.rb +0 -0
  54. data/tests/config.rb +2 -0
  55. data/tests/helper.rb +15 -0
  56. data/tests/test_content_encoding.rb +0 -0
  57. data/tests/test_content_type.rb +0 -0
  58. data/tests/test_crc_check.rb +184 -0
  59. data/tests/test_custom_headers.rb +14 -6
  60. data/tests/test_encoding.rb +0 -0
  61. data/tests/test_large_file.rb +0 -0
  62. data/tests/test_multipart.rb +0 -0
  63. data/tests/test_object_acl.rb +0 -0
  64. data/tests/test_object_key.rb +18 -0
  65. data/tests/test_object_url.rb +20 -0
  66. data/tests/test_resumable.rb +0 -0
  67. metadata +33 -12
@@ -28,11 +28,15 @@ module Aliyun
28
28
  # KEY SECRET,如果不填则会尝试匿名访问
29
29
  # @option opts [Boolean] :cname [可选] 指定endpoint是否是用户绑
30
30
  # 定的域名
31
+ # @option opts [Boolean] :upload_crc_enable [可选]指定上传处理
32
+ # 是否开启CRC校验,默认为开启(true)
33
+ # @option opts [Boolean] :download_crc_enable [可选]指定下载处理
34
+ # 是否开启CRC校验,默认为不开启(false)
31
35
  # @option opts [String] :sts_token [可选] 指定STS的
32
36
  # SecurityToken,如果指定,则使用STS授权访问
33
- # @option opts [Fixnum] :open_timeout [可选] 指定建立连接的超时
37
+ # @option opts [Integer] :open_timeout [可选] 指定建立连接的超时
34
38
  # 时间,默认为10秒
35
- # @option opts [Fixnum] :read_timeout [可选] 指定等待响应的超时
39
+ # @option opts [Integer] :read_timeout [可选] 指定等待响应的超时
36
40
  # 时间,默认为120秒
37
41
  # @example 标准endpoint
38
42
  # oss-cn-hangzhou.aliyuncs.com
@@ -66,6 +70,7 @@ module Aliyun
66
70
  # @param opts [Hash] 创建Bucket的属性(可选)
67
71
  # @option opts [:location] [String] 指定bucket所在的区域,默认为oss-cn-hangzhou
68
72
  def create_bucket(name, opts = {})
73
+ Util.ensure_bucket_name_valid(name)
69
74
  @protocol.create_bucket(name, opts)
70
75
  end
71
76
 
@@ -73,6 +78,7 @@ module Aliyun
73
78
  # @param name [String] Bucket名字
74
79
  # @note 如果要删除的Bucket不为空(包含有object),则删除会失败
75
80
  def delete_bucket(name)
81
+ Util.ensure_bucket_name_valid(name)
76
82
  @protocol.delete_bucket(name)
77
83
  end
78
84
 
@@ -80,6 +86,7 @@ module Aliyun
80
86
  # @param name [String] Bucket名字
81
87
  # @return [Boolean] 如果Bucket存在则返回true,否则返回false
82
88
  def bucket_exists?(name)
89
+ Util.ensure_bucket_name_valid(name)
83
90
  exist = false
84
91
 
85
92
  begin
@@ -98,6 +105,7 @@ module Aliyun
98
105
  # @param name [String] Bucket名字
99
106
  # @return [Bucket] Bucket对象
100
107
  def get_bucket(name)
108
+ Util.ensure_bucket_name_valid(name)
101
109
  Bucket.new({:name => name}, @protocol)
102
110
  end
103
111
 
@@ -11,7 +11,8 @@ module Aliyun
11
11
 
12
12
  attrs :endpoint, :cname, :sts_token,
13
13
  :access_key_id, :access_key_secret,
14
- :open_timeout, :read_timeout
14
+ :open_timeout, :read_timeout,
15
+ :download_crc_enable, :upload_crc_enable
15
16
 
16
17
  def initialize(opts = {})
17
18
  super(opts)
@@ -19,6 +20,8 @@ module Aliyun
19
20
  @access_key_id = @access_key_id.strip if @access_key_id
20
21
  @access_key_secret = @access_key_secret.strip if @access_key_secret
21
22
  normalize_endpoint if endpoint
23
+ @upload_crc_enable = (@upload_crc_enable == 'false' || @upload_crc_enable == false) ? false : true
24
+ @download_crc_enable = (@download_crc_enable == 'true' || @download_crc_enable == true) ? true : false
22
25
  end
23
26
 
24
27
  private
@@ -114,7 +114,7 @@ module Aliyun
114
114
 
115
115
  parts = sync_get_all_parts
116
116
  # concat all part files into the target file
117
- File.open(@file, 'w') do |w|
117
+ File.open(@file, 'wb') do |w|
118
118
  parts.sort{ |x, y| x[:number] <=> y[:number] }.each do |p|
119
119
  File.open(get_part_file(p)) do |r|
120
120
  w.write(r.read(READ_SIZE)) until r.eof?
@@ -177,7 +177,7 @@ module Aliyun
177
177
  logger.debug("Begin download part: #{p}")
178
178
 
179
179
  part_file = get_part_file(p)
180
- File.open(part_file, 'w') do |w|
180
+ File.open(part_file, 'wb') do |w|
181
181
  @protocol.get_object(
182
182
  bucket, object,
183
183
  @options.merge(range: p[:range])) { |chunk| w.write(chunk) }
@@ -64,6 +64,12 @@ module Aliyun
64
64
  class ClientError < Common::Exception
65
65
  end # ClientError
66
66
 
67
+ ##
68
+ # CrcInconsistentError will be raised after a upload operation,
69
+ # when the local crc is inconsistent with the response crc from server.
70
+ #
71
+ class CrcInconsistentError < Common::Exception; end
72
+
67
73
  ##
68
74
  # FileInconsistentError happens in a resumable upload transaction,
69
75
  # when the file to upload has changed during the uploading
@@ -43,10 +43,14 @@ module Aliyun
43
43
  # A stream is any class that responds to :read(bytes, outbuf)
44
44
  #
45
45
  class StreamWriter
46
- def initialize
46
+ attr_reader :data_crc
47
+
48
+ def initialize(crc_enable = false, init_crc = 0)
47
49
  @buffer = ""
48
50
  @producer = Fiber.new { yield self if block_given? }
49
51
  @producer.resume
52
+ @data_crc = init_crc.to_i
53
+ @crc_enable = crc_enable
50
54
  end
51
55
 
52
56
  def read(bytes = nil, outbuf = nil)
@@ -84,6 +88,8 @@ module Aliyun
84
88
  # "". ios.read(positive-integer) returns nil.
85
89
  return nil if ret.empty? && !bytes.nil? && bytes > 0
86
90
 
91
+ @data_crc = Aliyun::OSS::Util.crc(ret, @data_crc) if @crc_enable
92
+
87
93
  ret
88
94
  end
89
95
 
@@ -99,37 +105,12 @@ module Aliyun
99
105
  false
100
106
  end
101
107
 
102
- def inspect
103
- "@buffer: " + @buffer[0, 32].inspect + "...#{@buffer.size} bytes"
104
- end
105
- end
106
-
107
- # RestClient requires the payload to respones to :read(bytes)
108
- # and return a stream.
109
- # We are not doing the real read here, just return a
110
- # readable stream for RestClient playload.rb treats it as:
111
- # def read(bytes=nil)
112
- # @stream.read(bytes)
113
- # end
114
- # alias :to_s :read
115
- # net_http_do_request(http, req, payload ? payload.to_s : nil,
116
- # &@block_response)
117
- class StreamPayload
118
- def initialize(&block)
119
- @stream = StreamWriter.new(&block)
120
- end
121
-
122
- def read(bytes = nil)
123
- @stream
124
- end
125
-
126
108
  def close
127
109
  end
128
110
 
129
- def closed?
130
- false
111
+ def inspect
112
+ "@buffer: " + @buffer[0, 32].inspect + "...#{@buffer.size} bytes"
131
113
  end
132
-
133
114
  end
134
115
 
135
116
  include Common::Logging
@@ -140,12 +121,13 @@ module Aliyun
140
121
 
141
122
  def get_request_url(bucket, object)
142
123
  url = @config.endpoint.dup
124
+ url.query = nil
125
+ url.fragment = nil
143
126
  isIP = !!(url.host =~ Resolv::IPv4::Regex)
144
127
  url.host = "#{bucket}." + url.host if bucket && !@config.cname && !isIP
145
128
  url.path = '/'
146
129
  url.path << "#{bucket}/" if bucket && isIP
147
- url.path << "#{CGI.escape(object)}" if object
148
-
130
+ url.path << CGI.escape(object) if object
149
131
  url.to_s
150
132
  end
151
133
 
@@ -276,44 +258,46 @@ module Aliyun
276
258
  headers[:params] = (sub_res || {}).merge(http_options[:query] || {})
277
259
 
278
260
  block_response = ->(r) { handle_response(r, &block) } if block
279
- r = RestClient::Request.execute(
261
+ request = RestClient::Request.new(
280
262
  :method => verb,
281
263
  :url => get_request_url(bucket, object),
282
264
  :headers => headers,
283
265
  :payload => http_options[:body],
284
266
  :block_response => block_response,
285
267
  :open_timeout => @config.open_timeout || OPEN_TIMEOUT,
286
- :timeout => @config.read_timeout || READ_TIMEOUT
287
- ) do |response, request, result, &blk|
288
-
289
- if response.code >= 300
290
- e = ServerError.new(response)
268
+ :read_timeout => @config.read_timeout || READ_TIMEOUT
269
+ )
270
+ response = request.execute do |resp, &blk|
271
+ if resp.code >= 300
272
+ e = ServerError.new(resp)
291
273
  logger.error(e.to_s)
292
274
  raise e
293
275
  else
294
- response.return!(request, result, &blk)
276
+ resp.return!(&blk)
295
277
  end
296
278
  end
297
279
 
298
280
  # If streaming read_body is used, we need to create the
299
281
  # RestClient::Response ourselves
300
- unless r.is_a?(RestClient::Response)
301
- if r.code.to_i >= 300
302
- r = RestClient::Response.create(
303
- RestClient::Request.decode(r['content-encoding'], r.body),
304
- r, nil, nil)
305
- e = ServerError.new(r)
282
+ unless response.is_a?(RestClient::Response)
283
+ if response.code.to_i >= 300
284
+ body = response.body
285
+ if RestClient::version < '2.1.0'
286
+ body = RestClient::Request.decode(response['content-encoding'], response.body)
287
+ end
288
+ response = RestClient::Response.create(body, response, request)
289
+ e = ServerError.new(response)
306
290
  logger.error(e.to_s)
307
291
  raise e
308
292
  end
309
- r = RestClient::Response.create(nil, r, nil, nil)
310
- r.return!
293
+ response = RestClient::Response.create(nil, response, request)
294
+ response.return!
311
295
  end
312
296
 
313
- logger.debug("Received HTTP response, code: #{r.code}, headers: " \
314
- "#{r.headers}, body: #{r.body}")
297
+ logger.debug("Received HTTP response, code: #{response.code}, headers: " \
298
+ "#{response.headers}, body: #{response.body}")
315
299
 
316
- r
300
+ response
317
301
  end
318
302
 
319
303
  def get_user_agent
File without changes
@@ -30,7 +30,7 @@ module Aliyun
30
30
  md5= Util.get_content_md5(states.to_json)
31
31
 
32
32
  @mutex.synchronize {
33
- File.open(file, 'w') {
33
+ File.open(file, 'wb') {
34
34
  |f| f.write(states.merge(md5: md5).to_json)
35
35
  }
36
36
  }
File without changes
@@ -7,6 +7,7 @@ require 'time'
7
7
  module Aliyun
8
8
  module OSS
9
9
 
10
+
10
11
  ##
11
12
  # Protocol implement the OSS Open API which is low-level. User
12
13
  # should refer to {OSS::Client} for normal use.
@@ -345,10 +346,10 @@ module Aliyun
345
346
  xml.Date Time.utc(
346
347
  r.expiry.year, r.expiry.month, r.expiry.day)
347
348
  .iso8601.sub('Z', '.000Z')
348
- elsif r.expiry.is_a?(Fixnum)
349
+ elsif r.expiry.is_a?(Integer)
349
350
  xml.Days r.expiry
350
351
  else
351
- fail ClientError, "Expiry must be a Date or Fixnum."
352
+ fail ClientError, "Expiry must be a Date or Integer."
352
353
  end
353
354
  }
354
355
  }
@@ -534,9 +535,10 @@ module Aliyun
534
535
  headers[CALLBACK_HEADER] = opts[:callback].serialize
535
536
  end
536
537
 
538
+ payload = HTTP::StreamWriter.new(@config.upload_crc_enable, opts[:init_crc], &block)
537
539
  r = @http.put(
538
540
  {:bucket => bucket_name, :object => object_name},
539
- {:headers => headers, :body => HTTP::StreamPayload.new(&block)})
541
+ {:headers => headers, :body => payload})
540
542
 
541
543
  if r.code == 203
542
544
  e = CallbackError.new(r)
@@ -544,6 +546,11 @@ module Aliyun
544
546
  raise e
545
547
  end
546
548
 
549
+ if @config.upload_crc_enable && !r.headers[:x_oss_hash_crc64ecma].nil?
550
+ data_crc = payload.data_crc
551
+ Aliyun::OSS::Util.crc_check(data_crc, r.headers[:x_oss_hash_crc64ecma], 'put')
552
+ end
553
+
547
554
  logger.debug('Done put object')
548
555
  end
549
556
 
@@ -586,9 +593,19 @@ module Aliyun
586
593
 
587
594
  headers.merge!(to_lower_case(opts[:headers])) if opts.key?(:headers)
588
595
 
596
+ payload = HTTP::StreamWriter.new(
597
+ @config.upload_crc_enable && !opts[:init_crc].nil?, opts[:init_crc], &block)
598
+
589
599
  r = @http.post(
590
600
  {:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
591
- {:headers => headers, :body => HTTP::StreamPayload.new(&block)})
601
+ {:headers => headers, :body => payload})
602
+
603
+ if @config.upload_crc_enable &&
604
+ !r.headers[:x_oss_hash_crc64ecma].nil? &&
605
+ !opts[:init_crc].nil?
606
+ data_crc = payload.data_crc
607
+ Aliyun::OSS::Util.crc_check(data_crc, r.headers[:x_oss_hash_crc64ecma], 'append')
608
+ end
592
609
 
593
610
  logger.debug('Done append object')
594
611
 
@@ -759,11 +776,22 @@ module Aliyun
759
776
  rewrites[:expires].httpdate if rewrites.key?(:expires)
760
777
  end
761
778
 
779
+ data_crc = opts[:init_crc].nil? ? 0 : opts[:init_crc]
762
780
  r = @http.get(
763
781
  {:bucket => bucket_name, :object => object_name,
764
782
  :sub_res => sub_res},
765
783
  {:headers => headers}
766
- ) { |chunk| yield chunk if block_given? }
784
+ ) do |chunk|
785
+ if block_given?
786
+ # crc enable and no range and oss server support crc
787
+ data_crc = Aliyun::OSS::Util.crc(chunk, data_crc) if @config.download_crc_enable && range.nil?
788
+ yield chunk
789
+ end
790
+ end
791
+
792
+ if @config.download_crc_enable && range.nil? && !r.headers[:x_oss_hash_crc64ecma].nil?
793
+ Aliyun::OSS::Util.crc_check(data_crc, r.headers[:x_oss_hash_crc64ecma], 'get')
794
+ end
767
795
 
768
796
  h = r.headers
769
797
  metas = {}
@@ -1082,9 +1110,16 @@ module Aliyun
1082
1110
  "#{object_name}, txn id: #{txn_id}, part No: #{part_no}")
1083
1111
 
1084
1112
  sub_res = {'partNumber' => part_no, 'uploadId' => txn_id}
1113
+
1114
+ payload = HTTP::StreamWriter.new(@config.upload_crc_enable, &block)
1085
1115
  r = @http.put(
1086
1116
  {:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
1087
- {:body => HTTP::StreamPayload.new(&block)})
1117
+ {:body => payload})
1118
+
1119
+ if @config.upload_crc_enable && !r.headers[:x_oss_hash_crc64ecma].nil?
1120
+ data_crc = payload.data_crc
1121
+ Aliyun::OSS::Util.crc_check(data_crc, r.headers[:x_oss_hash_crc64ecma], 'put')
1122
+ end
1088
1123
 
1089
1124
  logger.debug("Done upload part")
1090
1125
 
@@ -1359,12 +1394,26 @@ module Aliyun
1359
1394
  @http.get_request_url(bucket, object)
1360
1395
  end
1361
1396
 
1397
+ # Get bucket/object resource path
1398
+ # @param [String] bucket the bucket name
1399
+ # @param [String] object the bucket name
1400
+ # @return [String] resource path for the bucket/object
1401
+ def get_resource_path(bucket, object = nil)
1402
+ @http.get_resource_path(bucket, object)
1403
+ end
1404
+
1362
1405
  # Get user's access key id
1363
1406
  # @return [String] the access key id
1364
1407
  def get_access_key_id
1365
1408
  @config.access_key_id
1366
1409
  end
1367
1410
 
1411
+ # Get user's access key secret
1412
+ # @return [String] the access key secret
1413
+ def get_access_key_secret
1414
+ @config.access_key_secret
1415
+ end
1416
+
1368
1417
  # Get user's STS token
1369
1418
  # @return [String] the STS token
1370
1419
  def get_sts_token
@@ -1378,6 +1427,18 @@ module Aliyun
1378
1427
  Util.sign(@config.access_key_secret, string_to_sign)
1379
1428
  end
1380
1429
 
1430
+ # Get the download crc status
1431
+ # @return true(download crc enable) or false(download crc disable)
1432
+ def download_crc_enable
1433
+ @config.download_crc_enable
1434
+ end
1435
+
1436
+ # Get the upload crc status
1437
+ # @return true(upload crc enable) or false(upload crc disable)
1438
+ def upload_crc_enable
1439
+ @config.upload_crc_enable
1440
+ end
1441
+
1381
1442
  private
1382
1443
 
1383
1444
  # Parse body content to xml document
@@ -1470,7 +1531,7 @@ module Aliyun
1470
1531
  def get_bytes_range(range)
1471
1532
  if range &&
1472
1533
  (!range.is_a?(Array) || range.size != 2 ||
1473
- !range.at(0).is_a?(Fixnum) || !range.at(1).is_a?(Fixnum))
1534
+ !range.at(0).is_a?(Integer) || !range.at(1).is_a?(Integer))
1474
1535
  fail ClientError, "Range must be an array containing 2 Integers."
1475
1536
  end
1476
1537
 
@@ -1493,7 +1554,6 @@ module Aliyun
1493
1554
  result
1494
1555
  end
1495
1556
  end
1496
-
1497
1557
  end # Protocol
1498
1558
  end # OSS
1499
1559
  end # Aliyun
@@ -107,10 +107,10 @@ module Aliyun
107
107
  # * id [String] the unique id of a rule
108
108
  # * enabled [Boolean] whether to enable this rule
109
109
  # * prefix [String] the prefix objects to apply this rule
110
- # * expiry [Date] or [Fixnum] the expire time of objects
110
+ # * expiry [Date] or [Integer] the expire time of objects
111
111
  # * if expiry is a Date, it specifies the absolute date to
112
112
  # expire objects
113
- # * if expiry is a Fixnum, it specifies the relative date to
113
+ # * if expiry is a Integer, it specifies the relative date to
114
114
  # expire objects: how many days after the object's last
115
115
  # modification time to expire the object
116
116
  # @example Specify expiry as Date
File without changes
@@ -7,6 +7,7 @@ require 'digest/md5'
7
7
 
8
8
  module Aliyun
9
9
  module OSS
10
+
10
11
  ##
11
12
  # Util functions to help generate formatted Date, signatures,
12
13
  # etc.
@@ -75,6 +76,29 @@ module Aliyun
75
76
  end
76
77
  end
77
78
 
79
+ # Get a crc value of the data
80
+ def crc(data, init_crc = 0)
81
+ CrcX::crc64(init_crc, data, data.size)
82
+ end
83
+
84
+ # Calculate a value of the crc1 combine with crc2.
85
+ def crc_combine(crc1, crc2, len2)
86
+ CrcX::crc64_combine(crc1, crc2, len2)
87
+ end
88
+
89
+ def crc_check(crc_a, crc_b, operation)
90
+ if crc_a.nil? || crc_b.nil? || crc_a.to_i != crc_b.to_i
91
+ logger.error("The crc of #{operation} between client and oss is not inconsistent. crc_a=#{crc_a} crc_b=#{crc_b}")
92
+ fail CrcInconsistentError.new("The crc of #{operation} between client and oss is not inconsistent.")
93
+ end
94
+ end
95
+
96
+ def ensure_bucket_name_valid(name)
97
+ unless (name =~ %r|^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$|)
98
+ fail ClientError, "The bucket name is invalid."
99
+ end
100
+ end
101
+
78
102
  end # self
79
103
  end # Util
80
104
  end # OSS
@@ -86,4 +110,4 @@ class String
86
110
  return true if self =~ /^true$/i
87
111
  false
88
112
  end
89
- end
113
+ end