gem_mirror 0.1.0.pre
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +19 -0
- data/README.md +85 -0
- data/exe/gem_mirror +7 -0
- data/gem_mirror.gemspec +55 -0
- data/lib/gem_mirror.rb +31 -0
- data/lib/gem_mirror/cli.rb +67 -0
- data/lib/gem_mirror/cli/checksum.rb +43 -0
- data/lib/gem_mirror/cli/index.rb +31 -0
- data/lib/gem_mirror/cli/init.rb +23 -0
- data/lib/gem_mirror/cli/update.rb +25 -0
- data/lib/gem_mirror/configuration.rb +140 -0
- data/lib/gem_mirror/gem.rb +66 -0
- data/lib/gem_mirror/gems_fetcher.rb +254 -0
- data/lib/gem_mirror/mirror_directory.rb +61 -0
- data/lib/gem_mirror/mirror_file.rb +65 -0
- data/lib/gem_mirror/source.rb +106 -0
- data/lib/gem_mirror/version.rb +6 -0
- data/lib/gem_mirror/versions_fetcher.rb +32 -0
- data/lib/gem_mirror/versions_file.rb +66 -0
- data/template/config.rb +27 -0
- metadata +290 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemMirror
|
4
|
+
##
|
5
|
+
# The Gem class contains data about a Gem such as the name, requirement as
|
6
|
+
# well as providing some methods to more easily extract the specific version
|
7
|
+
# number.
|
8
|
+
#
|
9
|
+
# @!attribute [r] name
|
10
|
+
# @return [String]
|
11
|
+
# @!attribute [r] requirement
|
12
|
+
# @return [Gem::Requirement]
|
13
|
+
#
|
14
|
+
class Gem
|
15
|
+
attr_reader :name, :requirement
|
16
|
+
|
17
|
+
##
|
18
|
+
# Returns a `Gem::Version` instance based on the specified requirement.
|
19
|
+
#
|
20
|
+
# @param [Gem::Requirement] requirement
|
21
|
+
# @return [Gem::Version]
|
22
|
+
#
|
23
|
+
def self.version_for(requirement)
|
24
|
+
::Gem::Version.new(requirement.requirements.max.last.version)
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# @param [String] name
|
29
|
+
# @param [Gem::Requirement] requirement
|
30
|
+
#
|
31
|
+
def initialize(name, requirement = nil)
|
32
|
+
requirement ||= ::Gem::Requirement.default
|
33
|
+
|
34
|
+
requirement = ::Gem::Requirement.new(requirement) if requirement.is_a?(String)
|
35
|
+
|
36
|
+
@name = name
|
37
|
+
@requirement = requirement
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# @return [Gem::Version]
|
42
|
+
#
|
43
|
+
def version
|
44
|
+
@version ||= self.class.version_for(requirement)
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# @return [TrueClass|FalseClass]
|
49
|
+
#
|
50
|
+
def version?
|
51
|
+
version && !version.segments.reject(&:zero?).empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Returns the filename of the Gemfile.
|
56
|
+
#
|
57
|
+
# @param [String] gem_version
|
58
|
+
# @return [String]
|
59
|
+
#
|
60
|
+
def filename(gem_version = nil)
|
61
|
+
gem_version ||= version.to_s
|
62
|
+
|
63
|
+
"#{name}-#{gem_version}.gem"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemMirror
|
4
|
+
##
|
5
|
+
# The GemsFetcher class is responsible for downloading Gems from an external
|
6
|
+
# source as well as downloading all the associated dependencies.
|
7
|
+
#
|
8
|
+
# @!attribute [r] source
|
9
|
+
# @return [Source]
|
10
|
+
# @!attribute [r] versions_file
|
11
|
+
# @return [GemMirror::VersionsFile]
|
12
|
+
#
|
13
|
+
class GemsFetcher
|
14
|
+
attr_reader :source, :versions_file
|
15
|
+
|
16
|
+
##
|
17
|
+
# @param [Source] source
|
18
|
+
# @param [GemMirror::VersionsFile] versions_file
|
19
|
+
#
|
20
|
+
def initialize(source, versions_file)
|
21
|
+
@source = source
|
22
|
+
@versions_file = versions_file
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Fetches the Gems and all associated dependencies.
|
27
|
+
#
|
28
|
+
def fetch
|
29
|
+
source.gems.each do |gem|
|
30
|
+
versions_for(gem).each do |version|
|
31
|
+
filename = gem.filename(version)
|
32
|
+
begin
|
33
|
+
satisfied = gem.requirement.satisfied_by?(version)
|
34
|
+
rescue StandardError
|
35
|
+
logger.debug("Error determining is requirement satisfied for #{filename}")
|
36
|
+
end
|
37
|
+
name = gem.name
|
38
|
+
|
39
|
+
if gem_exists?(filename) || ignore_gem?(name, version) || !satisfied
|
40
|
+
logger.debug("Skipping #{filename}")
|
41
|
+
next
|
42
|
+
end
|
43
|
+
|
44
|
+
# Prevent circular dependencies from messing things up.
|
45
|
+
configuration.ignore_gem(gem.name, version)
|
46
|
+
|
47
|
+
spec = fetch_specification(gem, version)
|
48
|
+
|
49
|
+
next unless spec
|
50
|
+
|
51
|
+
spec = load_specification(spec)
|
52
|
+
deps = dependencies_for(spec)
|
53
|
+
|
54
|
+
unless deps.empty?
|
55
|
+
logger.info("Fetching dependencies for #{filename}")
|
56
|
+
|
57
|
+
fetch_dependencies(deps)
|
58
|
+
end
|
59
|
+
|
60
|
+
logger.info("Fetching #{filename}")
|
61
|
+
|
62
|
+
gemfile = fetch_gem(gem, version)
|
63
|
+
|
64
|
+
configuration.mirror_directory.add_file(filename, gemfile) if gemfile
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Returns an Array containing the versions that should be fetched for a
|
71
|
+
# Gem.
|
72
|
+
#
|
73
|
+
# @param [GemMirror::Gem] gem
|
74
|
+
# @return [Array]
|
75
|
+
#
|
76
|
+
def versions_for(gem)
|
77
|
+
available = versions_file.versions_for(gem.name)
|
78
|
+
versions = gem.version? ? [gem.version] : available
|
79
|
+
available_names = available.map(&:to_s)
|
80
|
+
|
81
|
+
# Get rid of invalid versions. Due to Gem::Version having a custom ==
|
82
|
+
# method, which treats "3.4" the same as "3.4.0" we'll have to compare
|
83
|
+
# the versions as String instances.
|
84
|
+
versions = versions.select do |version|
|
85
|
+
available_names.include?(version.to_s)
|
86
|
+
end
|
87
|
+
|
88
|
+
versions = [available.last] if versions.empty?
|
89
|
+
|
90
|
+
versions
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Tries to download the specification for a Gem and version. This method
|
95
|
+
# returns the raw inflated data instead of an instance of
|
96
|
+
# `Gem::Specification`.
|
97
|
+
#
|
98
|
+
# @param [GemMirror::Gem] gem
|
99
|
+
# @param [Gem::Version] version
|
100
|
+
# @return [String]
|
101
|
+
#
|
102
|
+
def fetch_specification(gem, version)
|
103
|
+
specification = nil
|
104
|
+
filename = gem.filename(version)
|
105
|
+
|
106
|
+
begin
|
107
|
+
specification = source.fetch_specification(gem.name, version)
|
108
|
+
rescue StandardError => e
|
109
|
+
logger.error("Failed to retrieve #{filename}: #{e.message}")
|
110
|
+
logger.debug("Adding #{filename} to the list of ignored Gems")
|
111
|
+
|
112
|
+
configuration.ignore_gem(gem.name, version)
|
113
|
+
end
|
114
|
+
|
115
|
+
specification
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Tries to download the Gemfile for the specified Gem and version.
|
120
|
+
#
|
121
|
+
# @param [GemMirror::Gem] gem
|
122
|
+
# @param [Gem::Version] version
|
123
|
+
# @return [String]
|
124
|
+
#
|
125
|
+
def fetch_gem(gem, version)
|
126
|
+
gemfile = nil
|
127
|
+
filename = gem.filename(version)
|
128
|
+
|
129
|
+
begin
|
130
|
+
gemfile = source.fetch_gem(gem.name, version)
|
131
|
+
rescue StandardError => e
|
132
|
+
logger.error("Failed to retrieve #{filename}: #{e.message}")
|
133
|
+
logger.debug("Adding #{filename} to the list of ignored Gems")
|
134
|
+
|
135
|
+
configuration.ignore_gem(gem.name, version)
|
136
|
+
end
|
137
|
+
|
138
|
+
gemfile
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# Reads the inflated data of a Gemspec and returns the loaded specification
|
143
|
+
# instance.
|
144
|
+
#
|
145
|
+
# @param [String] raw_spec
|
146
|
+
# @return [Gem::Specification]
|
147
|
+
#
|
148
|
+
def load_specification(raw_spec)
|
149
|
+
stream = Zlib::Inflate.new
|
150
|
+
content = stream.inflate(raw_spec)
|
151
|
+
|
152
|
+
stream.finish
|
153
|
+
stream.close
|
154
|
+
|
155
|
+
Marshal.load(content)
|
156
|
+
end
|
157
|
+
|
158
|
+
##
|
159
|
+
# Fetches the Gem files for the specified dependencies.
|
160
|
+
#
|
161
|
+
# @param [Array] deps
|
162
|
+
#
|
163
|
+
def fetch_dependencies(deps)
|
164
|
+
self.class.new(source.updated(deps), versions_file).fetch
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Returns an Array containing all the dependencies of a given Gem
|
169
|
+
# specification.
|
170
|
+
#
|
171
|
+
# @param [Gem::Specification] spec
|
172
|
+
# @return [Array]
|
173
|
+
#
|
174
|
+
def dependencies_for(spec)
|
175
|
+
possible_dependencies = if configuration.development
|
176
|
+
spec.dependencies
|
177
|
+
else
|
178
|
+
spec.runtime_dependencies
|
179
|
+
end
|
180
|
+
|
181
|
+
dependencies = filter_dependencies(possible_dependencies)
|
182
|
+
|
183
|
+
assign_gem_versions(dependencies)
|
184
|
+
end
|
185
|
+
|
186
|
+
##
|
187
|
+
# Filters a list of dependencies based on whether or not they are ignored.
|
188
|
+
#
|
189
|
+
# @param [Array] possible_dependencies
|
190
|
+
# @return [Array]
|
191
|
+
#
|
192
|
+
def filter_dependencies(possible_dependencies)
|
193
|
+
dependencies = []
|
194
|
+
|
195
|
+
possible_dependencies.each do |dependency|
|
196
|
+
gem = Gem.new(dependency.name, dependency.requirement)
|
197
|
+
|
198
|
+
dependencies << gem unless ignore_gem?(gem.name, gem.version)
|
199
|
+
end
|
200
|
+
|
201
|
+
dependencies
|
202
|
+
end
|
203
|
+
|
204
|
+
##
|
205
|
+
# Processes a list of Gems and sets their versions to the latest one
|
206
|
+
# available in case no specific version is given.
|
207
|
+
#
|
208
|
+
# @param [Array] gems
|
209
|
+
# @return [Array]
|
210
|
+
#
|
211
|
+
def assign_gem_versions(gems)
|
212
|
+
gems.map do |gem|
|
213
|
+
unless gem.version?
|
214
|
+
latest = versions_file.versions_for(gem.name).last
|
215
|
+
gem = Gem.new(gem.name, latest.to_s) if latest
|
216
|
+
end
|
217
|
+
|
218
|
+
gem
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
##
|
223
|
+
# @see GemMirror::Configuration#logger
|
224
|
+
# @return [Logger]
|
225
|
+
#
|
226
|
+
def logger
|
227
|
+
configuration.logger
|
228
|
+
end
|
229
|
+
|
230
|
+
##
|
231
|
+
# @see GemMirror.configuration
|
232
|
+
#
|
233
|
+
def configuration
|
234
|
+
GemMirror.configuration
|
235
|
+
end
|
236
|
+
|
237
|
+
##
|
238
|
+
# Checks if a given Gem has already been downloaded.
|
239
|
+
#
|
240
|
+
# @param [String] filename
|
241
|
+
# @return [TrueClass|FalseClass]
|
242
|
+
#
|
243
|
+
def gem_exists?(filename)
|
244
|
+
configuration.mirror_directory.file_exists?(filename)
|
245
|
+
end
|
246
|
+
|
247
|
+
##
|
248
|
+
# @see GemMirror::Configuration#ignore_gem?
|
249
|
+
#
|
250
|
+
def ignore_gem?(*args)
|
251
|
+
configuration.ignore_gem?(*args)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemMirror
|
4
|
+
##
|
5
|
+
# The MirrorDirectory is used for dealing with files and directories that are
|
6
|
+
# mirrored from an external source.
|
7
|
+
#
|
8
|
+
# @!attribute [r] path
|
9
|
+
# @return [String]
|
10
|
+
#
|
11
|
+
class MirrorDirectory
|
12
|
+
attr_reader :path
|
13
|
+
|
14
|
+
##
|
15
|
+
# @param [String] path
|
16
|
+
#
|
17
|
+
def initialize(path)
|
18
|
+
@path = path
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Creates a new directory with the given name.
|
23
|
+
#
|
24
|
+
# @param [String] name
|
25
|
+
# @return [GemMirror::MirrorDirectory]
|
26
|
+
#
|
27
|
+
def add_directory(name)
|
28
|
+
full_path = File.join(path, name)
|
29
|
+
|
30
|
+
Dir.mkdir(full_path) unless File.directory?(full_path)
|
31
|
+
|
32
|
+
self.class.new(full_path)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Creates a new file with the given name and content.
|
37
|
+
#
|
38
|
+
# @param [String] name
|
39
|
+
# @param [String] content
|
40
|
+
# @return [Gem::MirrorFile]
|
41
|
+
#
|
42
|
+
def add_file(name, content)
|
43
|
+
full_path = File.join(path, name)
|
44
|
+
file = MirrorFile.new(full_path)
|
45
|
+
|
46
|
+
file.write(content)
|
47
|
+
|
48
|
+
file
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Checks if a given file exists in the current directory.
|
53
|
+
#
|
54
|
+
# @param [String] name
|
55
|
+
# @return [TrueClass|FalseClass]
|
56
|
+
#
|
57
|
+
def file_exists?(name)
|
58
|
+
File.file?(File.join(path, name))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemMirror
|
4
|
+
##
|
5
|
+
# Similar to {GemMirror::MirrorDirectory} the MirrorFile class is used to
|
6
|
+
# make it easier to read and write data in a directory that mirrors data from
|
7
|
+
# an external source.
|
8
|
+
#
|
9
|
+
# @!attribute [r] path
|
10
|
+
# @return [String]
|
11
|
+
#
|
12
|
+
class MirrorFile
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
##
|
16
|
+
# @param [String] path
|
17
|
+
#
|
18
|
+
def initialize(path)
|
19
|
+
@path = path
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Writes the specified content to the current file. Existing files are
|
24
|
+
# overwritten.
|
25
|
+
#
|
26
|
+
# @param [String] content
|
27
|
+
#
|
28
|
+
def write(content)
|
29
|
+
handle = File.open(path, "w")
|
30
|
+
|
31
|
+
handle.write(content)
|
32
|
+
handle.close
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Reads the content of the current file.
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
#
|
40
|
+
def read
|
41
|
+
handle = File.open(path, "r")
|
42
|
+
content = handle.read
|
43
|
+
|
44
|
+
handle.close
|
45
|
+
|
46
|
+
content
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Reads the contents of a Gzip encoded file.
|
51
|
+
#
|
52
|
+
# @return [String]
|
53
|
+
#
|
54
|
+
def read_gzip
|
55
|
+
content = nil
|
56
|
+
|
57
|
+
Zlib::GzipReader.open(path) do |gz|
|
58
|
+
content = gz.read
|
59
|
+
gz.close
|
60
|
+
end
|
61
|
+
|
62
|
+
content
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|