gem_mirror 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -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