1require 'uri'
2require 'fileutils'
3
4class Gem::Source
5  FILES = {
6    :released   => 'specs',
7    :latest     => 'latest_specs',
8    :prerelease => 'prerelease_specs',
9  }
10
11  def initialize(uri)
12    unless uri.kind_of? URI
13      uri = URI.parse(uri.to_s)
14    end
15
16    @uri = uri
17    @api_uri = nil
18  end
19
20  attr_reader :uri
21
22  def api_uri
23    require 'rubygems/remote_fetcher'
24    @api_uri ||= Gem::RemoteFetcher.fetcher.api_endpoint uri
25  end
26
27  def <=>(other)
28    if !@uri
29      return 0 unless other.uri
30      return -1
31    end
32
33    return 1 if !other.uri
34
35    @uri.to_s <=> other.uri.to_s
36  end
37
38  include Comparable
39
40  def ==(other)
41    case other
42    when self.class
43      @uri == other.uri
44    else
45      false
46    end
47  end
48
49  alias_method :eql?, :==
50
51  def hash
52    @uri.hash
53  end
54
55  ##
56  # Returns the local directory to write +uri+ to.
57
58  def cache_dir(uri)
59    # Correct for windows paths
60    escaped_path = uri.path.sub(/^\/([a-z]):\//i, '/\\1-/')
61    root = File.join Gem.user_home, '.gem', 'specs'
62    File.join root, "#{uri.host}%#{uri.port}", File.dirname(escaped_path)
63  end
64
65  def update_cache?
66    @update_cache ||=
67      begin
68        File.stat(Gem.user_home).uid == Process.uid
69      rescue Errno::ENOENT
70        false
71      end
72  end
73
74  def fetch_spec(name)
75    fetcher = Gem::RemoteFetcher.fetcher
76
77    spec_file_name = name.spec_name
78
79    uri = @uri + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}"
80
81    cache_dir = cache_dir uri
82
83    local_spec = File.join cache_dir, spec_file_name
84
85    if File.exist? local_spec then
86      spec = Gem.read_binary local_spec
87      spec = Marshal.load(spec) rescue nil
88      return spec if spec
89    end
90
91    uri.path << '.rz'
92
93    spec = fetcher.fetch_path uri
94    spec = Gem.inflate spec
95
96    if update_cache? then
97      FileUtils.mkdir_p cache_dir
98
99      open local_spec, 'wb' do |io|
100        io.write spec
101      end
102    end
103
104    # TODO: Investigate setting Gem::Specification#loaded_from to a URI
105    Marshal.load spec
106  end
107
108  ##
109  # Loads +type+ kind of specs fetching from +@uri+ if the on-disk cache is
110  # out of date.
111  #
112  # +type+ is one of the following:
113  #
114  # :released   => Return the list of all released specs
115  # :latest     => Return the list of only the highest version of each gem
116  # :prerelease => Return the list of all prerelease only specs
117  #
118
119  def load_specs(type)
120    file       = FILES[type]
121    fetcher    = Gem::RemoteFetcher.fetcher
122    file_name  = "#{file}.#{Gem.marshal_version}"
123    spec_path  = @uri + "#{file_name}.gz"
124    cache_dir  = cache_dir spec_path
125    local_file = File.join(cache_dir, file_name)
126    retried    = false
127
128    FileUtils.mkdir_p cache_dir if update_cache?
129
130    spec_dump = fetcher.cache_update_path spec_path, local_file, update_cache?
131
132    begin
133      Gem::NameTuple.from_list Marshal.load(spec_dump)
134    rescue ArgumentError
135      if update_cache? && !retried
136        FileUtils.rm local_file
137        retried = true
138        retry
139      else
140        raise Gem::Exception.new("Invalid spec cache file in #{local_file}")
141      end
142    end
143  end
144
145  def download(spec, dir=Dir.pwd)
146    fetcher = Gem::RemoteFetcher.fetcher
147    fetcher.download spec, @uri.to_s, dir
148  end
149end
150