summaryrefslogtreecommitdiff
path: root/lib/bundler/compact_index_client/cache.rb
blob: bedd7f8028a38cd9eefbe80dd7df69d79e8e8f14 (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
# frozen_string_literal: true

require_relative "gem_parser"

module Bundler
  class CompactIndexClient
    class Cache
      attr_reader :directory

      def initialize(directory, fetcher = nil)
        @directory = Pathname.new(directory).expand_path
        @updater = Updater.new(fetcher) if fetcher
        @mutex = Thread::Mutex.new
        @endpoints = Set.new

        @info_root = mkdir("info")
        @special_characters_info_root = mkdir("info-special-characters")
        @info_etag_root = mkdir("info-etags")
      end

      def names
        fetch("names", names_path, names_etag_path)
      end

      def versions
        fetch("versions", versions_path, versions_etag_path)
      end

      def info(name, remote_checksum = nil)
        path = info_path(name)

        if remote_checksum && remote_checksum != SharedHelpers.checksum_for_file(path, :MD5)
          fetch("info/#{name}", path, info_etag_path(name))
        else
          Bundler::CompactIndexClient.debug { "update skipped info/#{name} (#{remote_checksum ? "versions index checksum is nil" : "versions index checksum matches local"})" }
          read(path)
        end
      end

      def reset!
        @mutex.synchronize { @endpoints.clear }
      end

      private

      def names_path = directory.join("names")
      def names_etag_path = directory.join("names.etag")
      def versions_path = directory.join("versions")
      def versions_etag_path = directory.join("versions.etag")

      def info_path(name)
        name = name.to_s
        # TODO: converge this into the info_root by hashing all filenames like info_etag_path
        if /[^a-z0-9_-]/.match?(name)
          name += "-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}"
          @special_characters_info_root.join(name)
        else
          @info_root.join(name)
        end
      end

      def info_etag_path(name)
        name = name.to_s
        @info_etag_root.join("#{name}-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}")
      end

      def mkdir(name)
        directory.join(name).tap do |dir|
          SharedHelpers.filesystem_access(dir) do
            FileUtils.mkdir_p(dir)
          end
        end
      end

      def fetch(remote_path, path, etag_path)
        if already_fetched?(remote_path)
          Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" }
        else
          Bundler::CompactIndexClient.debug { "fetching #{remote_path}" }
          @updater&.update(remote_path, path, etag_path)
        end

        read(path)
      end

      def already_fetched?(remote_path)
        @mutex.synchronize { !@endpoints.add?(remote_path) }
      end

      def read(path)
        return unless path.file?
        SharedHelpers.filesystem_access(path, :read, &:read)
      end
    end
  end
end