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.
@@ -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