gem-mirror 0.0.1

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.
@@ -0,0 +1,65 @@
1
+ module GemMirror
2
+ module CLI
3
+ ##
4
+ # Hash containing the default Slop options.
5
+ #
6
+ # @return [Hash]
7
+ #
8
+ SLOP_OPTIONS = {
9
+ :strict => true,
10
+ :banner => 'Usage: gem-mirror [COMMAND] [OPTIONS]'
11
+ }
12
+
13
+ ##
14
+ # @return [Slop]
15
+ #
16
+ def self.options
17
+ return @options ||= default_options
18
+ end
19
+
20
+ ##
21
+ # Loads the specified configuration file or displays an error if it doesn't
22
+ # exist.
23
+ #
24
+ # @param [String] config_file
25
+ # @return [GemMirror::Configuration]
26
+ #
27
+ def self.load_configuration(config_file)
28
+ config_file ||= Configuration.default_configuration_file
29
+ config_file = File.expand_path(config_file, Dir.pwd)
30
+
31
+ unless File.file?(config_file)
32
+ abort "The configuration file #{config_file} does not exist"
33
+ end
34
+
35
+ require(config_file)
36
+ end
37
+
38
+ ##
39
+ # @return [Slop]
40
+ #
41
+ def self.default_options
42
+ return Slop.new(SLOP_OPTIONS.dup) do
43
+ separator "\nOptions:\n"
44
+
45
+ on :h, :help, 'Shows this help message' do
46
+ puts self
47
+ exit
48
+ end
49
+
50
+ on :v, :version, 'Shows the current version' do
51
+ puts CLI.version_information
52
+ end
53
+ end
54
+ end
55
+
56
+ ##
57
+ # Returns a String containing some platform/version related information.
58
+ #
59
+ # @return [String]
60
+ #
61
+ def self.version_information
62
+ return "gem-mirror v#{VERSION} on #{RUBY_DESCRIPTION}"
63
+ end
64
+ end # CLI
65
+ end # GemMirror
@@ -0,0 +1,41 @@
1
+ GemMirror::CLI.options.command 'checksum' do
2
+ banner 'Usage: gem-mirror checksum [OPTIONS]'
3
+ description 'Generates SHA512 checksums of all gems'
4
+ separator "\nOptions:\n"
5
+
6
+ on :h, :help, 'Shows this help message' do
7
+ puts self
8
+ exit
9
+ end
10
+
11
+ on :c=, :config=, 'Path to the configuration file'
12
+
13
+ run do |opts, args|
14
+ GemMirror::CLI.load_configuration(opts[:c])
15
+
16
+ config = GemMirror.configuration
17
+
18
+ unless File.directory?(config.checksums)
19
+ config.logger("The directory #{config.checksums} does not exist")
20
+ abort
21
+ end
22
+
23
+ unless File.directory?(config.destination)
24
+ config.logger("The directory #{config.destination} does not exist")
25
+ abort
26
+ end
27
+
28
+ Dir[File.join(config.gems_directory, '*.gem')].each do |gem|
29
+ basename = File.basename(gem)
30
+ name = basename + '.sha512'
31
+
32
+ config.logger.info("Creating checksum for #{basename}")
33
+
34
+ hash = Digest::SHA512.hexdigest(File.read(gem))
35
+ handle = File.open(File.join(config.checksums, name), 'w')
36
+
37
+ handle.write(hash)
38
+ handle.close
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,31 @@
1
+ GemMirror::CLI.options.command 'index' do
2
+ banner 'Usage: gem-mirror index [OPTIONS]'
3
+ description 'Indexes a list of Gems'
4
+ separator "\nOptions:\n"
5
+
6
+ on :h, :help, 'Shows this help message' do
7
+ puts self
8
+ exit
9
+ end
10
+
11
+ on :c=, :config=, 'Path to the configuration file'
12
+ on :l, :legacy, 'Build legacy indexes'
13
+
14
+ run do |opts, args|
15
+ GemMirror::CLI.load_configuration(opts[:c])
16
+
17
+ config = GemMirror.configuration
18
+
19
+ unless File.directory?(config.destination)
20
+ config.logger.error("The directory #{config.destination} does not exist")
21
+ abort
22
+ end
23
+
24
+ indexer = Gem::Indexer.new(config.destination, :build_legacy => opts[:l])
25
+ indexer.ui = Gem::SilentUI.new
26
+
27
+ config.logger.info("Generating indexes")
28
+
29
+ indexer.generate_index
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ GemMirror::CLI.options.command 'init' do
2
+ banner 'Usage: gem-mirror init [DIRECTORY] [OPTIONS]'
3
+ description 'Sets up a new mirror'
4
+ separator "\nOptions:\n"
5
+
6
+ on :h, :help, 'Shows this help message' do
7
+ puts self
8
+ exit
9
+ end
10
+
11
+ run do |opts, args|
12
+ directory = File.expand_path(args[0] || Dir.pwd)
13
+ template = GemMirror::Configuration.template_directory
14
+
15
+ Dir.mkdir(directory) unless File.directory?(directory)
16
+
17
+ FileUtils.cp_r(File.join(template, '.'), directory)
18
+
19
+ puts "Initialized empty mirror in #{directory}"
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ GemMirror::CLI.options.command 'update' do
2
+ banner 'Usage: gem-mirror update [OPTIONS]'
3
+ description 'Updates the list of Gems'
4
+ separator "\nOptions:\n"
5
+
6
+ on :h, :help, 'Shows this help message' do
7
+ puts self
8
+ exit
9
+ end
10
+
11
+ on :c=, :config=, 'Path to the configuration file'
12
+
13
+ run do |opts, args|
14
+ GemMirror::CLI.load_configuration(opts[:c])
15
+
16
+ GemMirror.configuration.sources.each do |source|
17
+ versions = GemMirror::VersionsFetcher.new(source).fetch
18
+ gems = GemMirror::GemsFetcher.new(source, versions)
19
+
20
+ gems.fetch
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,132 @@
1
+ module GemMirror
2
+ ##
3
+ # @return [GemMirror::Configuration]
4
+ #
5
+ def self.configuration
6
+ return @configuration ||= Configuration.new
7
+ end
8
+
9
+ ##
10
+ # Configuration class used for storing data about a mirror such as the
11
+ # destination directory, sources, ignored Gems, etc.
12
+ #
13
+ class Configuration < Confstruct::Configuration
14
+ ##
15
+ # @return [Logger]
16
+ #
17
+ def logger
18
+ return @logger ||= Logger.new(STDOUT)
19
+ end
20
+
21
+ ##
22
+ # @return [String]
23
+ #
24
+ def self.template_directory
25
+ return File.expand_path('../../../template', __FILE__)
26
+ end
27
+
28
+ ##
29
+ # @return [String]
30
+ #
31
+ def self.default_configuration_file
32
+ return File.expand_path('config.rb', Dir.pwd)
33
+ end
34
+
35
+ ##
36
+ # Returns the name of the directory that contains the quick
37
+ # specification files.
38
+ #
39
+ # @return [String]
40
+ #
41
+ def self.marshal_identifier
42
+ return "Marshal.#{marshal_version}"
43
+ end
44
+
45
+ ##
46
+ # Returns the name of the file that contains an index of all the versions.
47
+ #
48
+ # @return [String]
49
+ #
50
+ def self.versions_file
51
+ return "specs.#{marshal_version}.gz"
52
+ end
53
+
54
+ ##
55
+ # Returns a String containing the Marshal version.
56
+ #
57
+ # @return [String]
58
+ #
59
+ def self.marshal_version
60
+ return "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}"
61
+ end
62
+
63
+ ##
64
+ # @return [GemMirror::MirrorDirectory]
65
+ #
66
+ def mirror_directory
67
+ return @mirror_directory ||= MirrorDirectory.new(gems_directory)
68
+ end
69
+
70
+ ##
71
+ # @return [String]
72
+ #
73
+ def gems_directory
74
+ return File.join(destination, 'gems')
75
+ end
76
+
77
+ ##
78
+ # Returns a Hash containing various Gems to ignore and their versions.
79
+ #
80
+ # @return [Hash]
81
+ #
82
+ def ignored_gems
83
+ return @ignored_gems ||= Hash.new { |hash, key| hash[key] = [] }
84
+ end
85
+
86
+ ##
87
+ # Adds a Gem to the list of Gems to ignore.
88
+ #
89
+ # @param [String] name
90
+ # @param [String] version
91
+ #
92
+ def ignore_gem(name, version)
93
+ ignored_gems[name] ||= []
94
+ ignored_gems[name] << version
95
+ end
96
+
97
+ ##
98
+ # Checks if a Gem should be ignored.
99
+ #
100
+ # @param [String] name
101
+ # @param [String] version
102
+ # @return [TrueClass|FalseClass]
103
+ #
104
+ def ignore_gem?(name, version)
105
+ return ignored_gems[name].include?(version)
106
+ end
107
+
108
+ ##
109
+ # Returns a list of sources to mirror.
110
+ #
111
+ # @return [Array]
112
+ #
113
+ def sources
114
+ return @sources ||= []
115
+ end
116
+
117
+ ##
118
+ # Adds a new source to mirror.
119
+ #
120
+ # @param [String] name
121
+ # @param [String] url
122
+ # @param [Proc] block
123
+ # @yieldparam [GemMirror::Source] source
124
+ #
125
+ def source(name, url, &block)
126
+ source = Source.new(name, url)
127
+ source.instance_eval(&block)
128
+
129
+ sources << source
130
+ end
131
+ end # Configuration
132
+ end # GemMirror
@@ -0,0 +1,53 @@
1
+ module GemMirror
2
+ ##
3
+ # The Gem class contains data about a Gem such as the name, requirement as
4
+ # well as providing some methods to more easily extract the specific version
5
+ # number.
6
+ #
7
+ # @!attribute [r] name
8
+ # @return [String]
9
+ # @!attribute [r] requirement
10
+ # @return [Gem::Requirement]
11
+ #
12
+ class Gem
13
+ attr_reader :name, :requirement
14
+
15
+ ##
16
+ # Returns a `Gem::Version` instance based on the specified requirement.
17
+ #
18
+ # @param [Gem::Requirement] requirement
19
+ # @return [Gem::Version]
20
+ #
21
+ def self.version_for(requirement)
22
+ return ::Gem::Version.new(requirement.requirements.sort.last.last.version)
23
+ end
24
+
25
+ ##
26
+ # @param [String] name
27
+ # @param [Gem::Requirement] requirement
28
+ #
29
+ def initialize(name, requirement = nil)
30
+ @name = name
31
+ @requirement = requirement || ::Gem::Requirement.default
32
+ end
33
+
34
+ ##
35
+ # @return [Gem::Version]
36
+ #
37
+ def version
38
+ return @version ||= self.class.version_for(requirement)
39
+ end
40
+
41
+ ##
42
+ # Returns the filename of the Gemfile.
43
+ #
44
+ # @param [String] gem_version
45
+ # @return [String]
46
+ #
47
+ def filename(gem_version = nil)
48
+ gem_version ||= version.to_s
49
+
50
+ return "#{name}-#{gem_version}.gem"
51
+ end
52
+ end # Gem
53
+ end # GemMirror
@@ -0,0 +1,194 @@
1
+ module GemMirror
2
+ ##
3
+ # The GemsFetcher class is responsible for downloading Gems from an external
4
+ # source as well as downloading all the associated dependencies.
5
+ #
6
+ # @!attribute [r] source
7
+ # @return [Source]
8
+ # @!attribute [r] versions_file
9
+ # @return [GemMirror::VersionsFile]
10
+ #
11
+ class GemsFetcher
12
+ attr_reader :source, :versions_file
13
+
14
+ ##
15
+ # @param [Source] source
16
+ # @param [GemMirror::VersionsFile] versions_file
17
+ #
18
+ def initialize(source, versions_file)
19
+ @source = source
20
+ @versions_file = versions_file
21
+ end
22
+
23
+ ##
24
+ # Fetches the Gems and all associated dependencies.
25
+ #
26
+ def fetch
27
+ source.gems.each do |gem|
28
+ versions_file.versions_for(gem.name).each do |version|
29
+ filename = gem.filename(version)
30
+ satisfied = gem.requirement.satisfied_by?(version)
31
+ name = gem.name
32
+
33
+ if gem_exists?(filename) or ignore_gem?(name, version) or !satisfied
34
+ logger.debug("Skipping #{filename}")
35
+ next
36
+ end
37
+
38
+ # Prevent circular dependencies from messing things up.
39
+ configuration.ignore_gem(gem.name, version)
40
+
41
+ spec = fetch_specification(gem, version)
42
+
43
+ next unless spec
44
+
45
+ spec = load_specification(spec)
46
+ deps = dependencies_for(spec)
47
+
48
+ unless deps.empty?
49
+ logger.info("Fetching dependencies for #{filename}")
50
+
51
+ fetch_dependencies(deps)
52
+ end
53
+
54
+ logger.info("Fetching #{filename}")
55
+
56
+ gemfile = fetch_gem(gem, version)
57
+
58
+ if gemfile
59
+ configuration.mirror_directory.add_file(filename, gemfile)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Tries to download the specification for a Gem and version. This method
67
+ # returns the raw inflated data instead of an instance of
68
+ # `Gem::Specification`.
69
+ #
70
+ # @param [GemMirror::Gem] gem
71
+ # @param [Gem::Version] version
72
+ # @return [String]
73
+ #
74
+ def fetch_specification(gem, version)
75
+ specification = nil
76
+ filename = gem.filename(version)
77
+
78
+ begin
79
+ specification = source.fetch_specification(gem.name, version)
80
+ rescue => e
81
+ logger.error("Failed to retrieve #{filename}: #{e.message}")
82
+ logger.debug("Adding #{filename} to the list of ignored Gems")
83
+
84
+ configuration.ignore_gem(gem.name, version)
85
+ end
86
+
87
+ return specification
88
+ end
89
+
90
+ ##
91
+ # Tries to download the Gemfile for the specified Gem and version.
92
+ #
93
+ # @param [GemMirror::Gem] gem
94
+ # @param [Gem::Version] version
95
+ # @return [String]
96
+ #
97
+ def fetch_gem(gem, version)
98
+ gemfile = nil
99
+ filename = gem.filename(version)
100
+
101
+ begin
102
+ gemfile = source.fetch_gem(gem.name, version)
103
+ rescue => e
104
+ logger.error("Failed to retrieve #{filename}: #{e.message}")
105
+ logger.debug("Adding #{filename} to the list of ignored Gems")
106
+
107
+ configuration.ignore_gem(gem.name, version)
108
+ end
109
+
110
+ return gemfile
111
+ end
112
+
113
+ ##
114
+ # Reads the inflated data of a Gemspec and returns the loaded specification
115
+ # instance.
116
+ #
117
+ # @param [String] raw_spec
118
+ # @return [Gem::Specification]
119
+ #
120
+ def load_specification(raw_spec)
121
+ stream = Zlib::Inflate.new
122
+ content = stream.inflate(raw_spec)
123
+
124
+ stream.finish
125
+ stream.close
126
+
127
+ return Marshal.load(content)
128
+ end
129
+
130
+ ##
131
+ # Fetches the Gem files for the specified dependencies.
132
+ #
133
+ # @param [Array] deps
134
+ #
135
+ def fetch_dependencies(deps)
136
+ self.class.new(source.updated(deps), versions_file).fetch
137
+ end
138
+
139
+ ##
140
+ # Returns an Array containing all the dependencies of a given Gem
141
+ # specification.
142
+ #
143
+ # @param [Gem::Specification] spec
144
+ # @return [Array]
145
+ #
146
+ def dependencies_for(spec)
147
+ dependencies = []
148
+ possible_dependencies = configuration.development ? spec.dependencies \
149
+ : spec.runtime_dependencies
150
+
151
+ possible_dependencies.each do |dependency|
152
+ gem = Gem.new(dependency.name, dependency.requirement)
153
+
154
+ unless ignore_gem?(gem.name, gem.version)
155
+ dependencies << gem
156
+ end
157
+ end
158
+
159
+ return dependencies
160
+ end
161
+
162
+ ##
163
+ # @see GemMirror::Configuration#logger
164
+ # @return [Logger]
165
+ #
166
+ def logger
167
+ return configuration.logger
168
+ end
169
+
170
+ ##
171
+ # @see GemMirror.configuration
172
+ #
173
+ def configuration
174
+ return GemMirror.configuration
175
+ end
176
+
177
+ ##
178
+ # Checks if a given Gem has already been downloaded.
179
+ #
180
+ # @param [String] filename
181
+ # @return [TrueClass|FalseClass]
182
+ #
183
+ def gem_exists?(filename)
184
+ return configuration.mirror_directory.file_exists?(filename)
185
+ end
186
+
187
+ ##
188
+ # @see GemMirror::Configuration#ignore_gem?
189
+ #
190
+ def ignore_gem?(*args)
191
+ return configuration.ignore_gem?(*args)
192
+ end
193
+ end # GemsFetcher
194
+ end # GemMirror