summaryrefslogtreecommitdiff
path: root/lib/rubygems/source_info_cache.rb
blob: c84868a5f5ee6e0086d66bb5f2b0ae19c27a9081 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
require 'fileutils'

require 'rubygems'
require 'rubygems/source_info_cache_entry'
require 'rubygems/user_interaction'

# SourceInfoCache stores a copy of the gem index for each gem source.
#
# There are two possible cache locations, the system cache and the user cache:
# * The system cache is prefered if it is writable or can be created.
# * The user cache is used otherwise
#
# Once a cache is selected, it will be used for all operations.
# SourceInfoCache will not switch between cache files dynamically.
#
# Cache data is a Hash mapping a source URI to a SourceInfoCacheEntry.
#
#--
# To keep things straight, this is how the cache objects all fit together:
#
#   Gem::SourceInfoCache
#     @cache_data = {
#       source_uri => Gem::SourceInfoCacheEntry
#         @size => source index size
#         @source_index => Gem::SourceIndex
#       ...
#     }
#
class Gem::SourceInfoCache

  include Gem::UserInteraction

  @cache = nil
  @system_cache_file = nil
  @user_cache_file = nil

  def self.cache
    return @cache if @cache
    @cache = new
    @cache.refresh if Gem.configuration.update_sources
    @cache
  end

  def self.cache_data
    cache.cache_data
  end

  # Search all source indexes for +pattern+.
  def self.search(pattern, platform_only = false)
    cache.search pattern, platform_only
  end

  # Search all source indexes for +pattern+.  Only returns gems matching
  # Gem.platforms when +only_platform+ is true.  See #search_with_source.
  def self.search_with_source(pattern, only_platform = false)
    cache.search_with_source(pattern, only_platform)
  end

  def initialize # :nodoc:
    @cache_data = nil
    @cache_file = nil
    @dirty = false
  end

  # The most recent cache data.
  def cache_data
    return @cache_data if @cache_data
    cache_file # HACK writable check

    begin
      # Marshal loads 30-40% faster from a String, and 2MB on 20061116 is small
      data = File.open cache_file, 'rb' do |fp| fp.read end
      @cache_data = Marshal.load data

      @cache_data.each do |url, sice|
        next unless sice.is_a?(Hash)
        update
        cache = sice['cache']
        size  = sice['size']
        if cache.is_a?(Gem::SourceIndex) and size.is_a?(Numeric) then
          new_sice = Gem::SourceInfoCacheEntry.new cache, size
          @cache_data[url] = new_sice
        else # irreperable, force refetch.
          reset_cache_for(url)
        end
      end
      @cache_data
    rescue => e
      if Gem.configuration.really_verbose then
        say "Exception during cache_data handling: #{ex.class} - #{ex}"
        say "Cache file was: #{cache_file}"
        say "\t#{e.backtrace.join "\n\t"}"
      end
      reset_cache_data
    end
  end

  def reset_cache_for(url)
    say "Reseting cache for #{url}" if Gem.configuration.really_verbose

    sice = Gem::SourceInfoCacheEntry.new Gem::SourceIndex.new, 0
    sice.refresh url # HACK may be unnecessary, see ::cache and #refresh

    @cache_data[url] = sice
    @cache_data
  end

  def reset_cache_data
    @cache_data = {}
  end

  # The name of the cache file to be read
  def cache_file
    return @cache_file if @cache_file
    @cache_file = (try_file(system_cache_file) or
      try_file(user_cache_file) or
      raise "unable to locate a writable cache file")
  end

  # Write the cache to a local file (if it is dirty).
  def flush
    write_cache if @dirty
    @dirty = false
  end

  # Refreshes each source in the cache from its repository.
  def refresh
    Gem.sources.each do |source_uri|
      cache_entry = cache_data[source_uri]
      if cache_entry.nil? then
        cache_entry = Gem::SourceInfoCacheEntry.new nil, 0
        cache_data[source_uri] = cache_entry
      end

      update if cache_entry.refresh source_uri
    end

    flush
  end

  # Searches all source indexes for +pattern+.
  def search(pattern, platform_only = false)
    cache_data.map do |source_uri, sic_entry|
      next unless Gem.sources.include? source_uri
      sic_entry.source_index.search pattern, platform_only
    end.flatten.compact
  end

  # Searches all source indexes for +pattern+.  If +only_platform+ is true,
  # only gems matching Gem.platforms will be selected.  Returns an Array of
  # pairs containing the Gem::Specification found and the source_uri it was
  # found at.
  def search_with_source(pattern, only_platform = false)
    results = []

    cache_data.map do |source_uri, sic_entry|
      next unless Gem.sources.include? source_uri

      sic_entry.source_index.search(pattern, only_platform).each do |spec|
        results << [spec, source_uri]
      end
    end

    results
  end

  # Mark the cache as updated (i.e. dirty).
  def update
    @dirty = true
  end

  # The name of the system cache file.
  def system_cache_file
    self.class.system_cache_file
  end

  # The name of the system cache file. (class method)
  def self.system_cache_file
    @system_cache_file ||= Gem.default_system_source_cache_dir
  end

  # The name of the user cache file.
  def user_cache_file
    self.class.user_cache_file
  end

  # The name of the user cache file. (class method)
  def self.user_cache_file
    @user_cache_file ||=
      ENV['GEMCACHE'] || Gem.default_user_source_cache_dir
  end

  # Write data to the proper cache.
  def write_cache
    open cache_file, "wb" do |f|
      f.write Marshal.dump(cache_data)
    end
  end

  # Set the source info cache data directly.  This is mainly used for unit
  # testing when we don't want to read a file system to grab the cached source
  # index information.  The +hash+ should map a source URL into a
  # SourceInfoCacheEntry.
  def set_cache_data(hash)
    @cache_data = hash
    update
  end

  private

  # Determine if +fn+ is a candidate for a cache file.  Return fn if
  # it is.  Return nil if it is not.
  def try_file(fn)
    return fn if File.writable?(fn)
    return nil if File.exist?(fn)
    dir = File.dirname(fn)
    unless File.exist? dir then
      begin
        FileUtils.mkdir_p(dir)
      rescue RuntimeError, SystemCallError
        return nil
      end
    end
    if File.writable?(dir)
      File.open(fn, "wb") { |f| f << Marshal.dump({}) }
      return fn
    end
    nil
  end

end