aliyun-sdk 0.4.1 → 0.7.2

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