diff options
Diffstat (limited to 'lib/rubygems/source_info_cache.rb')
-rw-r--r-- | lib/rubygems/source_info_cache.rb | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/lib/rubygems/source_info_cache.rb b/lib/rubygems/source_info_cache.rb new file mode 100644 index 0000000000..0498e895a4 --- /dev/null +++ b/lib/rubygems/source_info_cache.rb @@ -0,0 +1,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 ||= File.join(Gem.dir, "source_cache") + 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'] || File.join(Gem.user_home, ".gem", "source_cache") + 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 + return nil + end + end + if File.writable?(dir) + File.open(fn, "wb") { |f| f << Marshal.dump({}) } + return fn + end + nil + end + +end + |