gemirro 1.6.0 → 3.0.0

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -1
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +3 -1
  5. data/Gemfile +2 -1
  6. data/Gemfile.lock +73 -61
  7. data/MANIFEST +0 -3
  8. data/bin/gemirro +1 -8
  9. data/gemirro.gemspec +4 -6
  10. data/lib/gemirro/cli/index.rb +48 -27
  11. data/lib/gemirro/cli/init.rb +48 -29
  12. data/lib/gemirro/cli/list.rb +37 -17
  13. data/lib/gemirro/cli/server.rb +173 -114
  14. data/lib/gemirro/cli/update.rb +42 -17
  15. data/lib/gemirro/cli.rb +64 -21
  16. data/lib/gemirro/configuration.rb +26 -20
  17. data/lib/gemirro/gem_version.rb +1 -0
  18. data/lib/gemirro/gem_version_collection.rb +4 -4
  19. data/lib/gemirro/gems_fetcher.rb +9 -9
  20. data/lib/gemirro/http.rb +115 -41
  21. data/lib/gemirro/indexer.rb +33 -232
  22. data/lib/gemirro/mirror_file.rb +3 -10
  23. data/lib/gemirro/server.rb +12 -43
  24. data/lib/gemirro/source.rb +2 -15
  25. data/lib/gemirro/utils.rb +22 -12
  26. data/lib/gemirro/version.rb +1 -1
  27. data/lib/gemirro/versions_fetcher.rb +7 -12
  28. data/lib/gemirro/versions_file.rb +26 -24
  29. data/lib/gemirro.rb +3 -7
  30. data/spec/gemirro/cli_spec.rb +49 -15
  31. data/spec/gemirro/configuration_spec.rb +9 -2
  32. data/spec/gemirro/gems_fetcher_spec.rb +2 -2
  33. data/spec/gemirro/http_spec.rb +58 -54
  34. data/spec/gemirro/indexer_spec.rb +5 -5
  35. data/spec/gemirro/server_spec.rb +30 -52
  36. data/spec/gemirro/source_spec.rb +2 -10
  37. data/spec/gemirro/versions_fetcher_spec.rb +3 -4
  38. data/spec/gemirro/versions_file_spec.rb +8 -32
  39. data/template/config.rb +1 -1
  40. metadata +29 -69
  41. data/template/public/latest_specs.4.8 +0 -0
  42. data/template/public/prerelease_specs.4.8 +0 -0
  43. data/template/public/specs.4.8 +0 -0
data/lib/gemirro/http.rb CHANGED
@@ -2,61 +2,135 @@
2
2
 
3
3
  module Gemirro
4
4
  ##
5
- # The Http class is responsible for executing GET request
6
- # to a specific url and return an response as an HTTP::Message
7
- #
8
- # @!attribute [r] client
9
- # @return [HTTPClient]
5
+ # The Http class is responsible for executing GET requests against a
6
+ # given URL, following redirects and reusing keep-alive connections.
10
7
  #
11
8
  class Http
12
- attr_accessor :client
13
-
14
9
  ##
15
- # Requests the given HTTP resource.
16
- #
17
- # @param [String] url
18
- # @return [HTTP::Message]
10
+ # Raised when a request comes back with a non-successful,
11
+ # non-redirect HTTP status.
19
12
  #
20
- def self.get(url)
21
- response = client.get(url, follow_redirect: true)
13
+ class Error < StandardError; end
22
14
 
23
- raise HTTPClient::BadResponseError, response.reason unless HTTP::Status.successful?(response.status)
15
+ MAX_REDIRECTS = 10
24
16
 
25
- response
26
- end
17
+ class << self
18
+ ##
19
+ # Requests the given HTTP resource, following redirects.
20
+ #
21
+ # @param [String] url
22
+ # @return [Net::HTTPResponse]
23
+ #
24
+ def get(url)
25
+ fetch(URI.parse(url), MAX_REDIRECTS)
26
+ end
27
27
 
28
- ##
29
- # @return [HTTPClient]
30
- #
31
- def self.client
32
- client ||= HTTPClient.new
33
- config = Utils.configuration
34
- if defined?(config.upstream_user)
35
- user = config.upstream_user
36
- password = config.upstream_password
37
- domain = config.upstream_domain
38
- client.set_auth(domain, user, password)
28
+ ##
29
+ # Closes and forgets every connection cached for the current
30
+ # Thread. Mostly useful for tests.
31
+ #
32
+ def reset!
33
+ pool.each_value { |connection| connection.finish if connection.started? }
34
+ pool.clear
35
+ end
36
+
37
+ private
38
+
39
+ ##
40
+ # @param [URI] uri
41
+ # @param [Integer] redirects_left
42
+ # @return [Net::HTTPResponse]
43
+ #
44
+ def fetch(uri, redirects_left)
45
+ response = connection_for(uri).request(build_request(uri))
46
+
47
+ case response
48
+ when Net::HTTPRedirection
49
+ raise Error, "Too many redirects for #{uri}" if redirects_left <= 0
50
+
51
+ fetch(URI.parse(response['location']), redirects_left - 1)
52
+ when Net::HTTPSuccess
53
+ response
54
+ else
55
+ raise Error, "#{response.code} #{response.message}"
56
+ end
57
+ end
58
+
59
+ ##
60
+ # @param [URI] uri
61
+ # @return [Net::HTTP::Get]
62
+ #
63
+ def build_request(uri)
64
+ request = Net::HTTP::Get.new(uri)
65
+ config = Utils.configuration
66
+ request.basic_auth(config.upstream_user, config.upstream_password) if defined?(config.upstream_user)
67
+
68
+ request
39
69
  end
40
70
 
41
- if defined?(config.proxy)
42
- proxy = config.proxy
43
- client.proxy = (proxy)
71
+ ##
72
+ # Returns a started, keep-alive connection for the given URI,
73
+ # reusing one already opened by the current Thread when possible.
74
+ #
75
+ # @param [URI] uri
76
+ # @return [Net::HTTP]
77
+ #
78
+ def connection_for(uri)
79
+ key = "#{uri.scheme}://#{uri.host}:#{uri.port}"
80
+ pool[key] ||= build_connection(uri)
44
81
  end
45
82
 
46
- # Use my own ca file for self signed cert
47
- if defined?(config.rootca)
48
- abort "The configuration file #{config.rootca} does not exist" unless File.file?(config.rootca)
49
- client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_PEER
50
- client.ssl_config.set_trust_ca(config.rootca)
51
- elsif defined?(config.verify_mode)
52
- client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE unless config.verify_mode
83
+ ##
84
+ # Connection pool for the current Thread. Kept per-Thread so
85
+ # sockets are never shared/interleaved across concurrent requests.
86
+ #
87
+ # @return [Hash]
88
+ #
89
+ def pool
90
+ Thread.current[:gemirro_http_pool] ||= {}
53
91
  end
54
92
 
55
- # Enforce base auth
56
- if defined?(config.basic_auth) && config.basic_auth
57
- client.www_auth.basic_auth.force_auth = (true)
93
+ ##
94
+ # @param [URI] uri
95
+ # @return [Net::HTTP]
96
+ #
97
+ def build_connection(uri)
98
+ config = Utils.configuration
99
+ http = Net::HTTP.new(uri.host, uri.port, *proxy_args(config))
100
+ http.use_ssl = uri.scheme == 'https'
101
+ configure_ssl(http, config)
102
+
103
+ http.start
104
+ end
105
+
106
+ ##
107
+ # @param [Net::HTTP] http
108
+ # @param [Gemirro::Configuration] config
109
+ #
110
+ def configure_ssl(http, config)
111
+ if defined?(config.rootca)
112
+ abort "The configuration file #{config.rootca} does not exist" unless File.file?(config.rootca)
113
+
114
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
115
+ http.ca_file = config.rootca
116
+ elsif defined?(config.verify_mode) && !config.verify_mode
117
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
118
+ end
119
+ end
120
+
121
+ ##
122
+ # Splits a `proxy 'http://user:pass@host:port'` configuration
123
+ # value into the args expected by `Net::HTTP.new`.
124
+ #
125
+ # @param [Gemirro::Configuration] config
126
+ # @return [Array]
127
+ #
128
+ def proxy_args(config)
129
+ return [] unless defined?(config.proxy) && config.proxy
130
+
131
+ proxy_uri = URI.parse(config.proxy)
132
+ [proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password]
58
133
  end
59
- @client = client
60
134
  end
61
135
  end
62
136
  end
@@ -35,19 +35,16 @@ module Gemirro
35
35
  # @param [Hash] options Indexer options
36
36
  # @return [Array]
37
37
  ##
38
- def initialize(directory, options = {})
38
+ def initialize(options = {})
39
39
  require 'fileutils'
40
40
  require 'tmpdir'
41
41
  require 'zlib'
42
42
  require 'builder/xchar'
43
43
  require 'compact_index'
44
44
 
45
- options.merge!({ build_modern: true, build_compact: true })
45
+ options.merge!({ build_modern: false })
46
46
 
47
- @build_modern = options[:build_modern]
48
- @build_compact = options[:build_compact]
49
-
50
- @dest_directory = directory
47
+ @dest_directory = Gemirro.configuration.destination
51
48
  @directory =
52
49
  File.join(Dir.tmpdir, "gem_generate_index_#{rand(1_000_000_000)}")
53
50
 
@@ -69,22 +66,12 @@ module Gemirro
69
66
  @latest_index =
70
67
  File.join(@quick_dir, 'latest_index')
71
68
 
72
- @specs_index =
73
- File.join(@directory, "specs.#{::Gem.marshal_version}")
74
69
  @latest_specs_index =
75
70
  File.join(@directory, "latest_specs.#{::Gem.marshal_version}")
76
- @prerelease_specs_index =
77
- File.join(@directory, "prerelease_specs.#{::Gem.marshal_version}")
78
- @dest_specs_index =
79
- File.join(@dest_directory, "specs.#{::Gem.marshal_version}")
80
71
  @dest_latest_specs_index =
81
72
  File.join(@dest_directory, "latest_specs.#{::Gem.marshal_version}")
82
- @dest_prerelease_specs_index =
83
- File.join(@dest_directory, "prerelease_specs.#{::Gem.marshal_version}")
84
73
  @infos_dir =
85
74
  File.join(@dest_directory, 'info')
86
- @api_v1_dependencies_dir =
87
- File.join(@dest_directory, 'api', 'v1', 'dependencies')
88
75
 
89
76
  @files = []
90
77
  end
@@ -92,16 +79,9 @@ module Gemirro
92
79
  ##
93
80
  # Generate indices on the destination directory
94
81
  #
95
- def install_indices
96
- install_indicies
97
- end
98
-
99
- ##
100
- # Generate indicies on the destination directory
101
- #
102
82
  # @return [Array]
103
83
  #
104
- def install_indicies
84
+ def install_indices
105
85
  Utils.logger
106
86
  .debug("Downloading index into production dir #{@dest_directory}")
107
87
 
@@ -111,24 +91,19 @@ module Gemirro
111
91
  if files.include?(@quick_marshal_dir) && !files.include?(@quick_dir)
112
92
  files.delete @quick_marshal_dir
113
93
  FileUtils.mkdir_p(File.dirname(@quick_marshal_dir_base), verbose: verbose)
114
- FileUtils.rm_rf(@quick_marshal_dir_base, verbose: verbose)
94
+ if @quick_marshal_dir_base && File.exist?(@quick_marshal_dir_base)
95
+ FileUtils.rm_rf(@quick_marshal_dir_base, verbose: verbose)
96
+ end
115
97
  FileUtils.mv(@quick_marshal_dir, @quick_marshal_dir_base, verbose: verbose, force: true)
116
98
  end
117
99
 
118
100
  files.each do |path|
119
101
  file = path.sub(%r{^#{Regexp.escape @directory}/?}, '')
120
102
 
121
- if ["#{@specs_index}.gz",
122
- "#{@latest_specs_index}.gz",
123
- "#{@prerelease_specs_index}.gz"].include?(path)
124
- res = build_zlib_file(file, File.join(@directory, file), File.join(@dest_directory, file), true)
125
- next unless res
126
- else
127
- source_content = download_from_source(file)
128
- next if source_content.nil?
103
+ source_content = download_from_source(file)
104
+ next if source_content.nil?
129
105
 
130
- MirrorFile.new(File.join(@dest_directory, file)).write(source_content)
131
- end
106
+ MirrorFile.new(File.join(@dest_directory, file)).write(source_content)
132
107
 
133
108
  FileUtils.rm_rf(path)
134
109
  end
@@ -142,9 +117,9 @@ module Gemirro
142
117
  #
143
118
  def download_from_source(file)
144
119
  source_host = Gemirro.configuration.source.host
145
- Utils.logger.info("Download from source: #{file}")
120
+ Utils.logger.info("Download from source #{source_host}/#{file}")
146
121
  resp = Http.get("#{source_host}/#{File.basename(file)}")
147
- return unless resp.code == 200
122
+ return unless resp.code.to_i == 200
148
123
 
149
124
  resp.body
150
125
  end
@@ -155,104 +130,18 @@ module Gemirro
155
130
  # @return [Array]
156
131
  #
157
132
  def build_indices
158
- build_indicies
159
- end
160
-
161
- ##
162
- # Build indicies
163
- #
164
- # @return [Array]
165
- #
166
- def build_indicies
167
133
  specs = *map_gems_to_specs(gem_file_list)
168
134
  specs.select! { |s| s.instance_of?(::Gem::Specification) }
169
135
  ::Gem::Specification.dirs = []
170
136
  ::Gem::Specification.all = specs
171
137
 
172
- if ::Gem::VERSION >= '2.5.0'
173
- build_marshal_gemspecs(specs)
174
- build_modern_indices(specs) if @build_modern
175
- compress_indices
176
- else
177
- build_marshal_gemspecs
178
- build_modern_indicies if @build_modern
179
- compress_indicies
180
- end
181
-
182
- build_api_v1_dependencies(specs)
183
-
184
- return unless @build_compact
138
+ build_marshal_gemspecs(specs)
185
139
 
186
140
  build_compact_index_names
187
141
  build_compact_index_infos(specs)
188
142
  build_compact_index_versions(specs)
189
143
  end
190
144
 
191
- ##
192
- # Cache Modern Index endpoints /api/v1/dependencies?gems= and /api/v1/dependencies.json?gems=
193
- # This single request may include many fragments. server.rb determines which are required per request.
194
- #
195
- # @return nil
196
- #
197
- def build_api_v1_dependencies(specs, partial = false)
198
- FileUtils.mkdir_p(@api_v1_dependencies_dir)
199
-
200
- if partial
201
- specs.collect(&:name).uniq do |name|
202
- FileUtils.rm_rf(Dir.glob(File.join(@api_v1_dependencies_dir, "#{name}.*.*.list")))
203
- end
204
- else
205
- FileUtils.rm_rf(Dir.glob(File.join(@api_v1_dependencies_dir, '*.list')))
206
- end
207
-
208
- grouped_specs = specs.sort_by(&:name).group_by(&:name)
209
- grouped_specs.each_with_index do |(name, gem_versions), index|
210
- Utils.logger.info("[#{index + 1}/#{grouped_specs.size}]: Caching /api/v1/dependencies/#{name}")
211
-
212
- gem_versions =
213
- gem_versions.sort do |a, b|
214
- a.version <=> b.version
215
- end
216
-
217
- cg = []
218
- Parallel.each_with_index(
219
- gem_versions,
220
- in_threads: Utils.configuration.update_thread_count
221
- ) do |spec, index2|
222
- next if spec.nil?
223
-
224
- dependencies = spec.dependencies.select do |d|
225
- d.type == :runtime
226
- end
227
-
228
- dependencies = dependencies.collect do |d|
229
- [d.name.is_a?(Array) ? d.name.first : d.name, d.requirement.to_s]
230
- end
231
-
232
- cg[index2] =
233
- {
234
- name: spec.name,
235
- number: spec.version.to_s,
236
- platform: spec.platform,
237
- dependencies: dependencies
238
- }
239
- end
240
-
241
- Tempfile.create("api_v1_dependencies_#{name}.list") do |f|
242
- f.write Marshal.dump(cg)
243
- f.rewind
244
-
245
- FileUtils.cp(
246
- f.path,
247
- File.join(
248
- @api_v1_dependencies_dir,
249
- "#{name}.#{Digest::MD5.file(f.path).hexdigest}.#{Digest::SHA256.file(f.path).hexdigest}.list"
250
- )
251
- )
252
- end
253
- end
254
- end
255
-
256
145
  ##
257
146
  # Cache compact_index endpoint /names
258
147
  # Report all gems with versions available. Does not require opening spec files.
@@ -323,7 +212,7 @@ module Gemirro
323
212
  end
324
213
 
325
214
  Tempfile.create('versions.list') do |f|
326
- previous_versions_file = Dir.glob(File.join(@dest_directory, 'versions*.list')).last
215
+ previous_versions_file = Dir.glob(File.join(@dest_directory, 'versions.*.*.list')).last
327
216
 
328
217
  if partial && previous_versions_file
329
218
  versions_file = CompactIndex::VersionsFile.new(previous_versions_file)
@@ -336,7 +225,7 @@ module Gemirro
336
225
  f.write CompactIndex.versions(versions_file, cg)
337
226
  f.rewind
338
227
 
339
- FileUtils.rm_rf(Dir.glob(File.join(@dest_directory, 'versions*.list')))
228
+ FileUtils.rm_rf(Dir.glob(File.join(@dest_directory, 'versions.*.*.list')))
340
229
 
341
230
  FileUtils.cp(
342
231
  f.path,
@@ -359,7 +248,7 @@ module Gemirro
359
248
  # @return nil
360
249
  #
361
250
  def build_compact_index_infos(specs, partial = false)
362
- FileUtils.mkdir_p(@infos_dir)
251
+ FileUtils.mkdir_p(@infos_dir, verbose: verbose)
363
252
 
364
253
  if partial
365
254
  specs.collect(&:name).uniq do |name|
@@ -468,13 +357,8 @@ module Gemirro
468
357
  next
469
358
  end
470
359
 
471
- if ::Gem::VERSION >= '2.5.0'
472
- spec.abbreviate
473
- spec.sanitize
474
- else
475
- abbreviate(spec)
476
- sanitize(spec)
477
- end
360
+ spec.abbreviate
361
+ spec.sanitize
478
362
 
479
363
  spec
480
364
  rescue SignalException
@@ -507,14 +391,15 @@ module Gemirro
507
391
  indexed_gemfiles = Dir.glob('*.gemspec.rz', base: @quick_marshal_dir_base).collect { |x| x.gsub(/spec.rz$/, '') }
508
392
 
509
393
  @updated_gems = []
394
+
510
395
  # detect files manually added to public/gems
511
396
  @updated_gems += (present_gemfiles - indexed_gemfiles).collect { |x| File.join(@dest_directory, 'gems', x) }
512
397
  # detect files manually deleted from public/gems
513
398
  @updated_gems += (indexed_gemfiles - present_gemfiles).collect { |x| File.join(@dest_directory, 'gems', x) }
514
399
 
515
- specs_mtime =
400
+ versions_mtime =
516
401
  begin
517
- File.stat(@dest_specs_index).mtime
402
+ File.stat(Dir.glob(File.join(@dest_directory, 'versions*.list')).last).mtime
518
403
  rescue StandardError
519
404
  Time.at(0)
520
405
  end
@@ -525,7 +410,7 @@ module Gemirro
525
410
  gem_file_list.select do |gem|
526
411
  gem_mtime = File.stat(gem).mtime
527
412
  newest_mtime = gem_mtime if gem_mtime > newest_mtime
528
- gem_mtime > specs_mtime
413
+ gem_mtime > versions_mtime
529
414
  end
530
415
 
531
416
  @updated_gems.uniq!
@@ -538,7 +423,7 @@ module Gemirro
538
423
  specs = map_gems_to_specs(@updated_gems)
539
424
 
540
425
  # specs only includes latest discovered files.
541
- # /info/[gemname] and /api/v1/dependencies can not be rebuilt
426
+ # /info/[gemname] can not be rebuilt
542
427
  # incrementally, so retrive specs for all versions of these gems.
543
428
  gem_name_updates = specs.collect(&:name).uniq
544
429
  u2 =
@@ -549,108 +434,24 @@ module Gemirro
549
434
  Utils.logger.info('Reloading for /info/[gemname]')
550
435
  version_specs = map_gems_to_specs(u2)
551
436
 
552
- prerelease, released = specs.partition { |s| s.version.prerelease? }
553
-
554
437
  ::Gem::Specification.dirs = []
555
438
  ::Gem::Specification.all = *specs
556
- files =
557
- if ::Gem::VERSION >= '2.5.0'
558
- build_marshal_gemspecs specs
559
- else
560
- build_marshal_gemspecs
561
- end
562
-
563
- ::Gem.time('Updated indexes') do
564
- update_specs_index(
565
- released,
566
- @dest_specs_index,
567
- @specs_index
568
- )
569
- update_specs_index(
570
- released,
571
- @dest_latest_specs_index,
572
- @latest_specs_index
573
- )
574
- update_specs_index(
575
- prerelease,
576
- @dest_prerelease_specs_index,
577
- @prerelease_specs_index
578
- )
579
- end
580
-
581
- if ::Gem::VERSION >= '2.5.0'
582
- compress_indices
583
- else
584
- compress_indicies
585
- end
439
+ build_marshal_gemspecs specs
586
440
 
587
- build_api_v1_dependencies(version_specs, true)
588
-
589
- if @build_compact
590
- build_compact_index_names
591
- build_compact_index_infos(version_specs, true)
592
- build_compact_index_versions(specs, true)
593
- end
594
-
595
- Utils.logger.info("Updating production dir #{@dest_directory}") if verbose
596
- files << @specs_index
597
- files << "#{@specs_index}.gz"
598
- files << @latest_specs_index
599
- files << "#{@latest_specs_index}.gz"
600
- files << @prerelease_specs_index
601
- files << "#{@prerelease_specs_index}.gz"
602
-
603
- files.each do |path|
604
- file = path.sub(%r{^#{Regexp.escape @directory}/?}, '')
605
-
606
- if ["#{@specs_index}.gz", "#{@latest_specs_index}.gz", "#{@prerelease_specs_index}.gz"].include?(path)
607
- res = build_zlib_file(file, File.join(@directory, file), File.join(@dest_directory, file))
608
- next unless res
609
- else
610
- FileUtils.mv(
611
- File.join(@directory, file),
612
- File.join(@dest_directory, file),
613
- verbose: verbose,
614
- force: true
615
- )
616
- end
617
-
618
- File.utime(newest_mtime, newest_mtime, File.join(@dest_directory, file))
619
- end
620
- end
621
-
622
- def build_zlib_file(file, src_name, dst_name, from_source = false)
623
- content = Marshal.load(Zlib::GzipReader.open(src_name).read)
624
- create_zlib_file("#{dst_name}.local", content)
625
-
626
- return false if @only_origin
627
-
628
- if from_source
629
- source_content = download_from_source(file)
630
- source_content = Marshal.load(
631
- Zlib::GzipReader.new(
632
- StringIO.new(source_content)
633
- ).read
634
- )
635
- else
636
- source_content = Marshal.load(Zlib::GzipReader.open(dst_name).read)
637
- end
638
-
639
- return false if source_content.nil?
640
-
641
- new_content = source_content.concat(content).uniq
642
- create_zlib_file(dst_name, new_content)
441
+ build_compact_index_infos(version_specs, true)
442
+ build_compact_index_versions(specs, true)
443
+ build_compact_index_names
643
444
  end
644
445
 
645
- def create_zlib_file(dst_name, content)
646
- Tempfile.create(File.basename(dst_name)) do |f|
647
- gzf = Zlib::GzipWriter.new(f)
648
- gzf.write(Marshal.dump(content))
649
- gzf.close
446
+ def download_source_versions
447
+ Tempfile.create(File.basename(Gemirro.configuration.versions_file)) do |f|
448
+ f.write(download_from_source('versions'))
449
+ f.close
650
450
 
451
+ FileUtils.rm(Gemirro.configuration.versions_file, verbose: verbose)
651
452
  FileUtils.cp(
652
453
  f.path,
653
- dst_name,
454
+ Gemirro.configuration.versions_file,
654
455
  verbose: verbose
655
456
  )
656
457
  end
@@ -27,10 +27,8 @@ module Gemirro
27
27
  #
28
28
  def write(content)
29
29
  FileUtils.mkdir_p(File.dirname(@path))
30
- handle = File.open(@path, 'w')
31
-
32
- handle.write(content)
33
- handle.close
30
+ File.write(@path, content)
31
+ nil
34
32
  end
35
33
 
36
34
  ##
@@ -39,12 +37,7 @@ module Gemirro
39
37
  # @return [String]
40
38
  #
41
39
  def read
42
- handle = File.open(@path, 'r')
43
- content = handle.read
44
-
45
- handle.close
46
-
47
- content
40
+ File.read(@path)
48
41
  end
49
42
  end
50
43
  end