berkshelf 1.2.0.rc1 → 1.2.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.
- data/CHANGELOG.md +8 -0
- data/Gemfile +1 -1
- data/README.md +2 -0
- data/berkshelf.gemspec +4 -3
- data/features/step_definitions/filesystem_steps.rb +0 -1
- data/generator_files/Gemfile.erb +0 -3
- data/generator_files/Vagrantfile.erb +13 -1
- data/lib/berkshelf.rb +0 -1
- data/lib/berkshelf/berksfile.rb +11 -4
- data/lib/berkshelf/cached_cookbook.rb +2 -257
- data/lib/berkshelf/chef.rb +0 -1
- data/lib/berkshelf/chef/config.rb +3 -0
- data/lib/berkshelf/chef/cookbook.rb +0 -2
- data/lib/berkshelf/community_rest.rb +31 -6
- data/lib/berkshelf/cookbook_source.rb +5 -1
- data/lib/berkshelf/errors.rb +24 -0
- data/lib/berkshelf/git.rb +49 -1
- data/lib/berkshelf/init_generator.rb +1 -1
- data/lib/berkshelf/locations/chef_api_location.rb +6 -3
- data/lib/berkshelf/locations/path_location.rb +2 -0
- data/lib/berkshelf/version.rb +1 -1
- data/spec/spec_helper.rb +9 -2
- data/spec/support/chef_api.rb +1 -10
- data/spec/unit/berkshelf/cached_cookbook_spec.rb +37 -458
- data/spec/unit/berkshelf/git_spec.rb +119 -9
- data/spec/unit/berkshelf/init_generator_spec.rb +0 -1
- metadata +30 -24
- data/lib/berkshelf/chef/cookbook/metadata.rb +0 -556
- data/lib/berkshelf/chef/cookbook/syntax_check.rb +0 -158
- data/lib/berkshelf/chef/digester.rb +0 -67
- data/lib/berkshelf/mixin/checksum.rb +0 -16
- data/lib/berkshelf/mixin/params_validate.rb +0 -218
- data/lib/berkshelf/mixin/shell_out.rb +0 -23
- data/lib/berkshelf/uploader.rb +0 -80
- data/spec/unit/berkshelf/uploader_spec.rb +0 -27
- data/spec/unit/chef/cookbook/metadata_spec.rb +0 -5
- data/spec/unit/chef/digester_spec.rb +0 -41
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
# 1.2.0
|
2
|
+
- Remove Vagrant as a gem dependency
|
3
|
+
- Remove Chef as a gem dependency
|
4
|
+
- Add retries to downloads/uploads
|
5
|
+
- Speed optimizations to resolver
|
6
|
+
- Speed optimizations to downloading cookbooks
|
7
|
+
- Speed optimizations to uploading cookbooks
|
8
|
+
|
1
9
|
# 1.1.0
|
2
10
|
## new/improved commands
|
3
11
|
- `berks show` command: display the file path for the given cookbook's current version resolved by your Berksfile
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
Berkshelf
|
2
2
|
=========
|
3
|
+
[](http://badge.fury.io/rb/berkshelf)
|
3
4
|
[](https://travis-ci.org/RiotGames/berkshelf)
|
5
|
+
[](https://gemnasium.com/RiotGames/berkshelf)
|
4
6
|
[](https://codeclimate.com/github/RiotGames/berkshelf)
|
5
7
|
|
6
8
|
Manage a Cookbook or an Application's Cookbook dependencies
|
data/berkshelf.gemspec
CHANGED
@@ -32,14 +32,15 @@ Gem::Specification.new do |s|
|
|
32
32
|
s.add_dependency 'mixlib-shellout'
|
33
33
|
s.add_dependency 'mixlib-config'
|
34
34
|
s.add_dependency 'faraday', '>= 0.8.5'
|
35
|
-
s.add_dependency 'ridley', '>= 0.
|
36
|
-
s.add_dependency 'chozo', '>= 0.
|
37
|
-
s.add_dependency 'hashie'
|
35
|
+
s.add_dependency 'ridley', '>= 0.8.3'
|
36
|
+
s.add_dependency 'chozo', '>= 0.6.1'
|
37
|
+
s.add_dependency 'hashie', '>= 2.0.2'
|
38
38
|
s.add_dependency 'minitar'
|
39
39
|
s.add_dependency 'json', '>= 1.5.0'
|
40
40
|
s.add_dependency 'multi_json', '~> 1.5'
|
41
41
|
s.add_dependency 'solve', '>= 0.4.2'
|
42
42
|
s.add_dependency 'thor', '~> 0.16.0'
|
43
|
+
s.add_dependency 'retryable'
|
43
44
|
|
44
45
|
# Vagrant 1-0-stable compatability locks
|
45
46
|
s.add_dependency 'moneta', '~> 0.6.0'
|
data/generator_files/Gemfile.erb
CHANGED
@@ -1,4 +1,16 @@
|
|
1
|
-
|
1
|
+
begin
|
2
|
+
require 'berkshelf/vagrant'
|
3
|
+
rescue LoadError
|
4
|
+
puts "[WARNING] Berkshelf not found in your Vagrant's RubyGems but your Vagrantfile is attempting"
|
5
|
+
puts "[WARNING] to require the Berkshelf Vagrant plugin! Install the Berkshelf Vagrant plugin or"
|
6
|
+
puts "[WARNING] remove the 'require \"berkshelf/vagrant\"' line from the top of your Vagrantfile."
|
7
|
+
puts ""
|
8
|
+
puts "If you installed Vagrant by RubyGems:"
|
9
|
+
puts " Install Berkshelf by running: \"gem install berkshelf\""
|
10
|
+
puts "If you installed Vagrant by one of the pre-packaged installers:"
|
11
|
+
puts " Install Berkshelf by running: \"vagrant gem install berkshelf\""
|
12
|
+
puts ""
|
13
|
+
end
|
2
14
|
|
3
15
|
Vagrant::Config.run do |config|
|
4
16
|
# All Vagrant configuration is done here. The most common configuration
|
data/lib/berkshelf.rb
CHANGED
data/lib/berkshelf/berksfile.rb
CHANGED
@@ -183,7 +183,7 @@ module Berkshelf
|
|
183
183
|
raise CookbookNotFound, "No 'metadata.rb' found at #{path}"
|
184
184
|
end
|
185
185
|
|
186
|
-
metadata =
|
186
|
+
metadata = Ridley::Chef::Cookbook::Metadata.from_file(metadata_file.to_s)
|
187
187
|
|
188
188
|
name = if metadata.name.empty? || metadata.name.nil?
|
189
189
|
File.basename(File.dirname(metadata_file))
|
@@ -478,12 +478,17 @@ module Berkshelf
|
|
478
478
|
#
|
479
479
|
# @raise [UploadFailure] if you are uploading cookbooks with an invalid or not-specified client key
|
480
480
|
def upload(options = {})
|
481
|
-
|
481
|
+
conn = Ridley.new(options)
|
482
482
|
solution = resolve(options)
|
483
|
+
|
483
484
|
solution.each do |cb|
|
484
|
-
|
485
|
-
|
485
|
+
upload_opts = options.dup
|
486
|
+
upload_opts[:name] = cb.cookbook_name
|
487
|
+
|
488
|
+
Berkshelf.formatter.upload cb.cookbook_name, cb.version, upload_opts[:server_url]
|
489
|
+
conn.cookbook.upload(cb.path, upload_opts)
|
486
490
|
end
|
491
|
+
|
487
492
|
if options[:skip_dependencies]
|
488
493
|
missing_cookbooks = options.fetch(:cookbooks, nil) - solution.map(&:cookbook_name)
|
489
494
|
unless missing_cookbooks.empty?
|
@@ -496,6 +501,8 @@ module Berkshelf
|
|
496
501
|
msg = "Could not upload cookbooks: Missing Chef client key: '#{Berkshelf::Config.instance.chef.client_key}'."
|
497
502
|
msg << " Generate or update your Berkshelf configuration that contains a valid path to a Chef client key."
|
498
503
|
raise UploadFailure, msg
|
504
|
+
ensure
|
505
|
+
conn.terminate if conn && conn.alive?
|
499
506
|
end
|
500
507
|
|
501
508
|
# Finds a solution for the Berksfile and returns an array of CachedCookbooks.
|
@@ -1,29 +1,7 @@
|
|
1
1
|
module Berkshelf
|
2
2
|
# @author Jamie Winsor <reset@riotgames.com>
|
3
|
-
class CachedCookbook
|
3
|
+
class CachedCookbook < Ridley::Chef::Cookbook
|
4
4
|
class << self
|
5
|
-
include Berkshelf::Mixin::Checksum
|
6
|
-
|
7
|
-
# Creates a new instance of Berkshelf::CachedCookbook from a path on disk that
|
8
|
-
# contains a Cookbook. The name of the Cookbook will be determined first by the
|
9
|
-
# name attribute of the metadata.rb file if it is present. If the name attribute
|
10
|
-
# has not been set the Cookbook name will be determined by the basename of the
|
11
|
-
# given filepath.
|
12
|
-
#
|
13
|
-
# @param [#to_s] path
|
14
|
-
# a path on disk to the location of a Cookbook
|
15
|
-
#
|
16
|
-
# @return [Berkshelf::CachedCookbook]
|
17
|
-
def from_path(path)
|
18
|
-
path = Pathname.new(path)
|
19
|
-
metadata = Berkshelf::Chef::Cookbook::Metadata.from_file(path.join('metadata.rb'))
|
20
|
-
|
21
|
-
name = metadata.name.empty? ? File.basename(path) : metadata.name
|
22
|
-
metadata.name(name) if metadata.name.empty?
|
23
|
-
|
24
|
-
new(name, path, metadata)
|
25
|
-
end
|
26
|
-
|
27
5
|
# @param [#to_s] path
|
28
6
|
# a path on disk to the location of a Cookbook downloaded by the Downloader
|
29
7
|
#
|
@@ -35,248 +13,15 @@ module Berkshelf
|
|
35
13
|
cached_name = File.basename(path.to_s).slice(DIRNAME_REGEXP, 1)
|
36
14
|
return nil if cached_name.nil?
|
37
15
|
|
38
|
-
|
39
|
-
metadata.name(cached_name) if metadata.name.empty?
|
40
|
-
|
41
|
-
new(cached_name, path, metadata)
|
42
|
-
end
|
43
|
-
|
44
|
-
# @param [String] filepath
|
45
|
-
# a path on disk to the location of a file to checksum
|
46
|
-
#
|
47
|
-
# @return [String]
|
48
|
-
# a checksum that can be used to uniquely identify the file understood
|
49
|
-
# by a Chef Server.
|
50
|
-
def checksum(filepath)
|
51
|
-
Berkshelf::Chef::Digester.md5_checksum_for_file(filepath)
|
16
|
+
from_path(path, name: cached_name)
|
52
17
|
end
|
53
18
|
end
|
54
19
|
|
55
20
|
DIRNAME_REGEXP = /^(.+)-(.+)$/
|
56
|
-
CHEF_TYPE = "cookbook_version".freeze
|
57
|
-
CHEF_JSON_CLASS = "Chef::CookbookVersion".freeze
|
58
|
-
|
59
|
-
extend Forwardable
|
60
|
-
|
61
|
-
attr_reader :cookbook_name
|
62
|
-
attr_reader :path
|
63
|
-
attr_reader :metadata
|
64
|
-
|
65
|
-
# @return [Hashie::Mash]
|
66
|
-
# a Hashie::Mash containing Cookbook file category names as keys and an Array of Hashes
|
67
|
-
# containing metadata about the files belonging to that category. This is used
|
68
|
-
# to communicate what a Cookbook looks like when uploading to a Chef Server.
|
69
|
-
#
|
70
|
-
# example:
|
71
|
-
# {
|
72
|
-
# :recipes => [
|
73
|
-
# {
|
74
|
-
# name: "default.rb",
|
75
|
-
# path: "recipes/default.rb",
|
76
|
-
# checksum: "fb1f925dcd5fc4ebf682c4442a21c619",
|
77
|
-
# specificity: "default"
|
78
|
-
# }
|
79
|
-
# ]
|
80
|
-
# ...
|
81
|
-
# ...
|
82
|
-
# }
|
83
|
-
attr_reader :manifest
|
84
|
-
|
85
|
-
def_delegator :@metadata, :version
|
86
|
-
|
87
|
-
def initialize(name, path, metadata)
|
88
|
-
@cookbook_name = name
|
89
|
-
@path = Pathname.new(path)
|
90
|
-
@metadata = metadata
|
91
|
-
@files = Array.new
|
92
|
-
@manifest = Hashie::Mash.new(
|
93
|
-
recipes: Array.new,
|
94
|
-
definitions: Array.new,
|
95
|
-
libraries: Array.new,
|
96
|
-
attributes: Array.new,
|
97
|
-
files: Array.new,
|
98
|
-
templates: Array.new,
|
99
|
-
resources: Array.new,
|
100
|
-
providers: Array.new,
|
101
|
-
root_files: Array.new
|
102
|
-
)
|
103
|
-
|
104
|
-
load_files
|
105
|
-
end
|
106
|
-
|
107
|
-
# @return [String]
|
108
|
-
# the name of the cookbook and the version number separated by a dash (-).
|
109
|
-
#
|
110
|
-
# example:
|
111
|
-
# "nginx-0.101.2"
|
112
|
-
def name
|
113
|
-
"#{cookbook_name}-#{version}"
|
114
|
-
end
|
115
21
|
|
116
22
|
# @return [Hash]
|
117
23
|
def dependencies
|
118
24
|
metadata.recommendations.merge(metadata.dependencies)
|
119
25
|
end
|
120
|
-
|
121
|
-
# @return [Hash]
|
122
|
-
# an hash containing the checksums and expanded file paths of all of the
|
123
|
-
# files found in the instance of CachedCookbook
|
124
|
-
#
|
125
|
-
# example:
|
126
|
-
# {
|
127
|
-
# "da97c94bb6acb2b7900cbf951654fea3" => "/Users/reset/.berkshelf/nginx-0.101.2/README.md"
|
128
|
-
# }
|
129
|
-
def checksums
|
130
|
-
{}.tap do |checksums|
|
131
|
-
files.each do |file|
|
132
|
-
checksums[self.class.checksum(file)] = file
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
# @param [Symbol] category
|
138
|
-
# the category of file to generate metadata about
|
139
|
-
# @param [String] target
|
140
|
-
# the filepath to the file to get metadata information about
|
141
|
-
#
|
142
|
-
# @return [Hash]
|
143
|
-
# a Hash containing a name, path, checksum, and specificity key representing the
|
144
|
-
# metadata about a file contained in a Cookbook. This metadata is used when
|
145
|
-
# uploading a Cookbook's files to a Chef Server.
|
146
|
-
#
|
147
|
-
# example:
|
148
|
-
# {
|
149
|
-
# name: "default.rb",
|
150
|
-
# path: "recipes/default.rb",
|
151
|
-
# checksum: "fb1f925dcd5fc4ebf682c4442a21c619",
|
152
|
-
# specificity: "default"
|
153
|
-
# }
|
154
|
-
def file_metadata(category, target)
|
155
|
-
target = Pathname.new(target)
|
156
|
-
|
157
|
-
{
|
158
|
-
name: target.basename.to_s,
|
159
|
-
path: target.relative_path_from(path).to_s,
|
160
|
-
checksum: self.class.checksum(target),
|
161
|
-
specificity: file_specificity(category, target)
|
162
|
-
}
|
163
|
-
end
|
164
|
-
|
165
|
-
# Validates that this instance of CachedCookbook points to a valid location on disk that
|
166
|
-
# contains a cookbook which passes a Ruby and template syntax check. Raises an error if
|
167
|
-
# these assertions are not true.
|
168
|
-
#
|
169
|
-
# @return [Boolean]
|
170
|
-
# returns true if Cookbook is valid
|
171
|
-
def validate!
|
172
|
-
raise CookbookNotFound, "No Cookbook found at: #{path}" unless path.exist?
|
173
|
-
|
174
|
-
unless quietly { syntax_checker.validate_ruby_files }
|
175
|
-
raise CookbookSyntaxError, "Invalid ruby files in cookbook: #{name} (#{version})."
|
176
|
-
end
|
177
|
-
unless quietly { syntax_checker.validate_templates }
|
178
|
-
raise CookbookSyntaxError, "Invalid template files in cookbook: #{name} (#{version})."
|
179
|
-
end
|
180
|
-
|
181
|
-
true
|
182
|
-
end
|
183
|
-
|
184
|
-
def to_hash
|
185
|
-
result = manifest.dup
|
186
|
-
result['chef_type'] = 'cookbook_version'
|
187
|
-
result['name'] = name
|
188
|
-
result['cookbook_name'] = cookbook_name
|
189
|
-
result['version'] = version
|
190
|
-
result['metadata'] = metadata
|
191
|
-
result['chef_type']
|
192
|
-
result
|
193
|
-
end
|
194
|
-
|
195
|
-
def to_json(*a)
|
196
|
-
result = self.to_hash
|
197
|
-
result['json_class'] = chef_json_class
|
198
|
-
result['frozen?'] = false
|
199
|
-
result.to_json(*a)
|
200
|
-
end
|
201
|
-
|
202
|
-
def to_s
|
203
|
-
"#{cookbook_name} (#{version}) '#{path}'"
|
204
|
-
end
|
205
|
-
|
206
|
-
def <=>(other_cookbook)
|
207
|
-
[self.cookbook_name, self.version] <=> [other_cookbook.cookbook_name, other_cookbook.version]
|
208
|
-
end
|
209
|
-
|
210
|
-
private
|
211
|
-
|
212
|
-
attr_reader :files
|
213
|
-
|
214
|
-
def chef_type
|
215
|
-
CHEF_TYPE
|
216
|
-
end
|
217
|
-
|
218
|
-
def chef_json_class
|
219
|
-
CHEF_JSON_CLASS
|
220
|
-
end
|
221
|
-
|
222
|
-
def syntax_checker
|
223
|
-
@syntax_checker ||= Berkshelf::Chef::Cookbook::SyntaxCheck.new(path.to_s)
|
224
|
-
end
|
225
|
-
|
226
|
-
def load_files
|
227
|
-
load_shallow(:recipes, 'recipes', '*.rb')
|
228
|
-
load_shallow(:definitions, 'definitions', '*.rb')
|
229
|
-
load_shallow(:libraries, 'libraries', '*.rb')
|
230
|
-
load_shallow(:attributes, 'attributes', '*.rb')
|
231
|
-
load_recursively(:files, "files", "*")
|
232
|
-
load_recursively(:templates, "templates", "*")
|
233
|
-
load_recursively(:resources, "resources", "*.rb")
|
234
|
-
load_recursively(:providers, "providers", "*.rb")
|
235
|
-
load_root
|
236
|
-
end
|
237
|
-
|
238
|
-
def load_root
|
239
|
-
[].tap do |files|
|
240
|
-
Dir.glob(path.join('*'), File::FNM_DOTMATCH).each do |file|
|
241
|
-
next if File.directory?(file)
|
242
|
-
@files << file
|
243
|
-
@manifest[:root_files] << file_metadata(:root_files, file)
|
244
|
-
end
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
def load_recursively(category, category_dir, glob)
|
249
|
-
[].tap do |files|
|
250
|
-
file_spec = path.join(category_dir, '**', glob)
|
251
|
-
Dir.glob(file_spec, File::FNM_DOTMATCH).each do |file|
|
252
|
-
next if File.directory?(file)
|
253
|
-
@files << file
|
254
|
-
@manifest[category] << file_metadata(category, file)
|
255
|
-
end
|
256
|
-
end
|
257
|
-
end
|
258
|
-
|
259
|
-
def load_shallow(category, *path_glob)
|
260
|
-
[].tap do |files|
|
261
|
-
Dir[path.join(*path_glob)].each do |file|
|
262
|
-
@files << file
|
263
|
-
@manifest[category] << file_metadata(category, file)
|
264
|
-
end
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
# @param [Symbol] category
|
269
|
-
# @param [Pathname] target
|
270
|
-
#
|
271
|
-
# @return [String]
|
272
|
-
def file_specificity(category, target)
|
273
|
-
case category
|
274
|
-
when :files, :templates
|
275
|
-
relpath = target.relative_path_from(path).to_s
|
276
|
-
relpath.slice(/(.+)\/(.+)\/.+/, 2)
|
277
|
-
else
|
278
|
-
'default'
|
279
|
-
end
|
280
|
-
end
|
281
26
|
end
|
282
27
|
end
|
data/lib/berkshelf/chef.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'socket'
|
2
|
+
require 'tmpdir'
|
2
3
|
require 'berkshelf/mixin'
|
3
4
|
require 'mixlib/config'
|
4
5
|
|
@@ -81,6 +82,8 @@ module Berkshelf::Chef
|
|
81
82
|
cookbook_email "YOUR_EMAIL"
|
82
83
|
cookbook_license "reserved"
|
83
84
|
|
85
|
+
knife Hash.new
|
86
|
+
|
84
87
|
# history: prior to Chef 11, the cache implementation was based on
|
85
88
|
# moneta and configured via cache_options[:path]. Knife configs
|
86
89
|
# generated with Chef 11 will have `syntax_check_cache_path`, but older
|
@@ -2,7 +2,5 @@ module Berkshelf::Chef
|
|
2
2
|
# @author Jamie Winsor <reset@riotgames.com>
|
3
3
|
module Cookbook
|
4
4
|
autoload :Chefignore, 'berkshelf/chef/cookbook/chefignore'
|
5
|
-
autoload :Metadata, 'berkshelf/chef/cookbook/metadata'
|
6
|
-
autoload :SyntaxCheck, 'berkshelf/chef/cookbook/syntax_check'
|
7
5
|
end
|
8
6
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'open-uri'
|
2
|
+
require 'retryable'
|
2
3
|
|
3
4
|
module Berkshelf
|
4
5
|
# @author Jamie Winsor <reset@riotgames.com>
|
@@ -32,13 +33,35 @@ module Berkshelf
|
|
32
33
|
|
33
34
|
V1_API = 'http://cookbooks.opscode.com/api/v1/cookbooks'.freeze
|
34
35
|
|
36
|
+
# @return [String]
|
35
37
|
attr_reader :api_uri
|
36
|
-
|
37
|
-
|
38
|
-
|
38
|
+
# @return [Integer]
|
39
|
+
# how many retries to attempt on HTTP requests
|
40
|
+
attr_reader :retries
|
41
|
+
# @return [Float]
|
42
|
+
# time to wait between retries
|
43
|
+
attr_reader :retry_interval
|
44
|
+
|
45
|
+
# @param [String] uri (CommunityREST::V1_API)
|
46
|
+
# location of community site to connect to
|
47
|
+
#
|
48
|
+
# @option options [Integer] :retries (5)
|
49
|
+
# retry requests on 5XX failures
|
50
|
+
# @option options [Float] :retry_interval (0.5)
|
51
|
+
# how often we should pause between retries
|
52
|
+
def initialize(uri = V1_API, options = {})
|
53
|
+
options = options.reverse_merge(retries: 5, retry_interval: 0.5)
|
54
|
+
@api_uri = Addressable::URI.parse(uri)
|
55
|
+
@retries = options[:retries]
|
56
|
+
@retry_interval = options[:retry_interval]
|
39
57
|
|
40
58
|
builder = Faraday::Builder.new do |b|
|
41
59
|
b.response :json
|
60
|
+
b.request :retry,
|
61
|
+
max: @retries,
|
62
|
+
interval: @retry_interval,
|
63
|
+
exceptions: [Faraday::Error::TimeoutError]
|
64
|
+
|
42
65
|
b.adapter :net_http
|
43
66
|
end
|
44
67
|
|
@@ -123,10 +146,12 @@ module Berkshelf
|
|
123
146
|
local = Tempfile.new('community-rest-stream')
|
124
147
|
local.binmode
|
125
148
|
|
126
|
-
|
127
|
-
|
149
|
+
retryable(tries: retries, on: OpenURI::HTTPError, sleep: retry_interval) do
|
150
|
+
open(target, 'rb', headers) do |remote|
|
151
|
+
local.write(remote.read)
|
152
|
+
end
|
128
153
|
end
|
129
|
-
|
154
|
+
|
130
155
|
local
|
131
156
|
ensure
|
132
157
|
local.close(false) unless local.nil?
|