gemirro 0.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of gemirro might be problematic. Click here for more details.

Files changed (48) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +5 -0
  3. data/.rubocop.yml +24 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +674 -0
  7. data/MANIFEST +46 -0
  8. data/README.md +60 -0
  9. data/Rakefile +10 -0
  10. data/bin/gemirro +6 -0
  11. data/gemirro.gemspec +31 -0
  12. data/lib/gemirro.rb +37 -0
  13. data/lib/gemirro/cli.rb +63 -0
  14. data/lib/gemirro/cli/index.rb +24 -0
  15. data/lib/gemirro/cli/init.rb +17 -0
  16. data/lib/gemirro/cli/server.rb +19 -0
  17. data/lib/gemirro/cli/update.rb +18 -0
  18. data/lib/gemirro/configuration.rb +137 -0
  19. data/lib/gemirro/gem.rb +70 -0
  20. data/lib/gemirro/gems_fetcher.rb +126 -0
  21. data/lib/gemirro/http.rb +37 -0
  22. data/lib/gemirro/indexer.rb +56 -0
  23. data/lib/gemirro/mirror_directory.rb +59 -0
  24. data/lib/gemirro/mirror_file.rb +48 -0
  25. data/lib/gemirro/server.rb +164 -0
  26. data/lib/gemirro/source.rb +58 -0
  27. data/lib/gemirro/version.rb +5 -0
  28. data/lib/gemirro/versions_fetcher.rb +31 -0
  29. data/lib/gemirro/versions_file.rb +65 -0
  30. data/spec/gemirro/cli_spec.rb +51 -0
  31. data/spec/gemirro/configuration_spec.rb +88 -0
  32. data/spec/gemirro/gem_spec.rb +37 -0
  33. data/spec/gemirro/gems_fetcher_spec.rb +104 -0
  34. data/spec/gemirro/http_spec.rb +36 -0
  35. data/spec/gemirro/indexer_spec.rb +55 -0
  36. data/spec/gemirro/mirror_directory_spec.rb +37 -0
  37. data/spec/gemirro/mirror_file_spec.rb +23 -0
  38. data/spec/gemirro/server_spec.rb +96 -0
  39. data/spec/gemirro/source_spec.rb +44 -0
  40. data/spec/gemirro/versions_fetcher_spec.rb +25 -0
  41. data/spec/gemirro/versions_file_spec.rb +52 -0
  42. data/spec/spec_helper.rb +17 -0
  43. data/task/manifest.rake +9 -0
  44. data/task/rspec.rake +6 -0
  45. data/task/rubocop.rake +5 -0
  46. data/template/config.rb +25 -0
  47. data/template/public/gems/.gitkeep +0 -0
  48. metadata +230 -0
@@ -0,0 +1,70 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Gemirro
3
+ ##
4
+ # The Gem class contains data about a Gem such as the name, requirement as
5
+ # well as providing some methods to more easily extract the specific version
6
+ # number.
7
+ #
8
+ # @!attribute [r] name
9
+ # @return [String]
10
+ # @!attribute [r] requirement
11
+ # @return [Gem::Requirement]
12
+ #
13
+ class Gem
14
+ attr_reader :name, :requirement
15
+
16
+ ##
17
+ # Returns a `Gem::Version` instance based on the specified requirement.
18
+ #
19
+ # @param [Gem::Requirement] requirement
20
+ # @return [Gem::Version]
21
+ #
22
+ def self.version_for(requirement)
23
+ ::Gem::Version.new(requirement.requirements.sort.last.last.version)
24
+ end
25
+
26
+ ##
27
+ # @param [String] name
28
+ # @param [Gem::Requirement|String] requirement
29
+ #
30
+ def initialize(name, requirement = nil)
31
+ requirement ||= ::Gem::Requirement.default
32
+
33
+ if requirement.is_a?(String)
34
+ requirement = ::Gem::Requirement.new(requirement)
35
+ end
36
+
37
+ @name = name
38
+ @requirement = requirement
39
+ end
40
+
41
+ ##
42
+ # Returns the version
43
+ #
44
+ # @return [Gem::Version]
45
+ #
46
+ def version
47
+ @version ||= self.class.version_for(requirement)
48
+ end
49
+
50
+ ##
51
+ # Define if version exists
52
+ #
53
+ # @return [TrueClass|FalseClass]
54
+ #
55
+ def version?
56
+ version && !version.segments.reject { |s| s == 0 }.empty?
57
+ end
58
+
59
+ ##
60
+ # Returns the filename of the Gemfile.
61
+ #
62
+ # @param [String] gem_version
63
+ # @return [String]
64
+ #
65
+ def filename(gem_version = nil)
66
+ gem_version ||= version.to_s
67
+ "#{name}-#{gem_version}.gem"
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,126 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Gemirro
3
+ ##
4
+ # The GemsFetcher class is responsible for downloading Gems from an external
5
+ # source.
6
+ #
7
+ # @!attribute [r] source
8
+ # @return [Source]
9
+ # @!attribute [r] versions_file
10
+ # @return [Gemirro::VersionsFile]
11
+ #
12
+ class GemsFetcher
13
+ attr_reader :source, :versions_file
14
+
15
+ ##
16
+ # @param [Source] source
17
+ # @param [Gemirro::VersionsFile] versions_file
18
+ #
19
+ def initialize(source, versions_file)
20
+ @source = source
21
+ @versions_file = versions_file
22
+ end
23
+
24
+ ##
25
+ # Fetches the Gems.
26
+ #
27
+ def fetch
28
+ @source.gems.each do |gem|
29
+ versions_for(gem).each do |version|
30
+ filename = gem.filename(version)
31
+ satisfied = gem.requirement.satisfied_by?(version)
32
+ name = gem.name
33
+
34
+ if gem_exists?(filename) || ignore_gem?(name, version) || !satisfied
35
+ logger.debug("Skipping #{filename}")
36
+ next
37
+ end
38
+
39
+ configuration.ignore_gem(gem.name, version)
40
+ logger.info("Fetching #{filename}")
41
+ gemfile = fetch_gem(gem, version)
42
+ configuration.mirror_directory.add_file(filename, gemfile) if gemfile
43
+ end
44
+ end
45
+ end
46
+
47
+ ##
48
+ # Returns an Array containing the versions that should be fetched for a
49
+ # Gem.
50
+ #
51
+ # @param [Gemirro::Gem] gem
52
+ # @return [Array]
53
+ #
54
+ def versions_for(gem)
55
+ available = @versions_file.versions_for(gem.name)
56
+ versions = gem.version? ? [gem.version] : available
57
+ available_names = available.map(&:to_s)
58
+
59
+ # Get rid of invalid versions. Due to Gem::Version having a custom ==
60
+ # method, which treats "3.4" the same as "3.4.0" we'll have to compare
61
+ # the versions as String instances.
62
+ versions = versions.select do |version|
63
+ available_names.include?(version.to_s)
64
+ end
65
+
66
+ versions = [available.last] if versions.empty?
67
+
68
+ versions
69
+ end
70
+
71
+ ##
72
+ # Tries to download the Gemfile for the specified Gem and version.
73
+ #
74
+ # @param [Gemirro::Gem] gem
75
+ # @param [Gem::Version] version
76
+ # @return [String]
77
+ #
78
+ def fetch_gem(gem, version)
79
+ data = nil
80
+ filename = gem.filename(version)
81
+
82
+ begin
83
+ data = @source.fetch_gem(gem.name, version)
84
+ rescue => e
85
+ logger.error("Failed to retrieve #{filename}: #{e.message}")
86
+ logger.debug("Adding #{filename} to the list of ignored Gems")
87
+
88
+ configuration.ignore_gem(gem.name, version)
89
+ end
90
+
91
+ data
92
+ end
93
+
94
+ ##
95
+ # @see Gemirro::Configuration#logger
96
+ # @return [Logger]
97
+ #
98
+ def logger
99
+ configuration.logger
100
+ end
101
+
102
+ ##
103
+ # @see Gemirro.configuration
104
+ #
105
+ def configuration
106
+ Gemirro.configuration
107
+ end
108
+
109
+ ##
110
+ # Checks if a given Gem has already been downloaded.
111
+ #
112
+ # @param [String] filename
113
+ # @return [TrueClass|FalseClass]
114
+ #
115
+ def gem_exists?(filename)
116
+ configuration.mirror_directory.file_exists?(filename)
117
+ end
118
+
119
+ ##
120
+ # @see Gemirro::Configuration#ignore_gem?
121
+ #
122
+ def ignore_gem?(*args)
123
+ configuration.ignore_gem?(*args)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,37 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Gemirro
4
+ ##
5
+ # The Http class is responsible for executing GET request
6
+ # to a specific url and return an response as an HTTP::Message
7
+ #
8
+ # @!attribute [r] client
9
+ # @return [HTTPClient]
10
+ #
11
+ class Http
12
+ attr_accessor :client
13
+
14
+ ##
15
+ # Requests the given HTTP resource.
16
+ #
17
+ # @param [String] url
18
+ # @return [HTTP::Message]
19
+ #
20
+ def self.get(url)
21
+ response = client.get(url, follow_redirect: true)
22
+
23
+ unless HTTP::Status.successful?(response.status)
24
+ fail HTTPClient::BadResponseError, response.reason
25
+ end
26
+
27
+ response
28
+ end
29
+
30
+ ##
31
+ # @return [HTTPClient]
32
+ #
33
+ def self.client
34
+ @client ||= HTTPClient.new
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,56 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Gemirro
4
+ ##
5
+ # The Indexer class is responsible for downloading useful file directly
6
+ # on the source host, such as specs-*.*.gz, marshal information, etc...
7
+ #
8
+ # @!attribute [r] files
9
+ # @return [Array]
10
+ # @!attribute [r] quick_marshal_dir
11
+ # @return [String]
12
+ # @!attribute [r] directory
13
+ # @return [String]
14
+ # @!attribute [r] dest_directory
15
+ # @return [String]
16
+ #
17
+ class Indexer < ::Gem::Indexer
18
+ attr_accessor :files, :quick_marshal_dir, :directory, :dest_directory
19
+
20
+ ##
21
+ # Generate indicies on the destination directory
22
+ #
23
+ # @return [Array]
24
+ #
25
+ def install_indicies
26
+ verbose = ::Gem.configuration.really_verbose
27
+ say "Downloading index into production dir #{@dest_directory}" if verbose
28
+
29
+ files = @files
30
+ files.delete @quick_marshal_dir if files.include? @quick_dir
31
+
32
+ if files.include?(@quick_marshal_dir) && !files.include?(@quick_dir)
33
+ files.delete @quick_marshal_dir
34
+ dst_name = File.join(@dest_directory, @quick_marshal_dir_base)
35
+ FileUtils.mkdir_p(File.dirname(dst_name), verbose: verbose)
36
+ FileUtils.rm_rf(dst_name, verbose: verbose)
37
+ FileUtils.mv(@quick_marshal_dir, dst_name,
38
+ verbose: verbose, force: true)
39
+ end
40
+
41
+ files = files.map do |path|
42
+ path.sub(/^#{Regexp.escape @directory}\/?/, '')
43
+ end
44
+
45
+ files.each do |file|
46
+ dst_name = File.join @dest_directory, file
47
+ next if File.exist?(dst_name) &&
48
+ (File.mtime(dst_name) >= Time.now - 360)
49
+
50
+ resp = Http.get("#{Gemirro.configuration.source.host}/#{file}")
51
+ next unless resp.code == 200
52
+ MirrorFile.new(dst_name).write(resp.body)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Gemirro
3
+ ##
4
+ # The MirrorDirectory is used for dealing with files and directories that are
5
+ # mirrored from an external source.
6
+ #
7
+ # @!attribute [r] path
8
+ # @return [String]
9
+ #
10
+ class MirrorDirectory
11
+ attr_reader :path
12
+
13
+ ##
14
+ # @param [String] path
15
+ #
16
+ def initialize(path)
17
+ @path = path
18
+ end
19
+
20
+ ##
21
+ # Creates directory or directories with the given path.
22
+ #
23
+ # @param [String] dir_path
24
+ # @return [Gemirro::MirrorDirectory]
25
+ #
26
+ def add_directory(dir_path)
27
+ full_path = File.join(@path, dir_path)
28
+ FileUtils.mkdir_p(full_path) unless File.directory?(full_path)
29
+
30
+ self.class.new(full_path)
31
+ end
32
+
33
+ ##
34
+ # Creates a new file with the given name and content.
35
+ #
36
+ # @param [String] name
37
+ # @param [String] content
38
+ # @return [Gem::MirrorFile]
39
+ #
40
+ def add_file(name, content)
41
+ full_path = File.join(@path, name)
42
+ file = MirrorFile.new(full_path)
43
+
44
+ file.write(content)
45
+
46
+ file
47
+ end
48
+
49
+ ##
50
+ # Checks if a given file exists in the current directory.
51
+ #
52
+ # @param [String] name
53
+ # @return [TrueClass|FalseClass]
54
+ #
55
+ def file_exists?(name)
56
+ File.file?(File.join(@path, name))
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Gemirro
3
+ ##
4
+ # Similar to {Gemirro::MirrorDirectory} the MirrorFile class is used to
5
+ # make it easier to read and write data in a directory that mirrors data from
6
+ # an external source.
7
+ #
8
+ # @!attribute [r] path
9
+ # @return [String]
10
+ #
11
+ class MirrorFile
12
+ attr_reader :path
13
+
14
+ ##
15
+ # @param [String] path
16
+ #
17
+ def initialize(path)
18
+ @path = path
19
+ end
20
+
21
+ ##
22
+ # Writes the specified content to the current file. Existing files are
23
+ # overwritten.
24
+ #
25
+ # @param [String] content
26
+ #
27
+ def write(content)
28
+ handle = File.open(@path, 'w')
29
+
30
+ handle.write(content)
31
+ handle.close
32
+ end
33
+
34
+ ##
35
+ # Reads the content of the current file.
36
+ #
37
+ # @return [String]
38
+ #
39
+ def read
40
+ handle = File.open(@path, 'r')
41
+ content = handle.read
42
+
43
+ handle.close
44
+
45
+ content
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,164 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Gemirro
4
+ ##
5
+ # Launch TCPServer to easily download gems.
6
+ #
7
+ # @!attribute [r] server
8
+ # @return [TCPServer]
9
+ # @!attribute [r] destination
10
+ # @return [String]
11
+ # @!attribute [r] versions_fetcher
12
+ # @return [VersionsFetcher]
13
+ # @!attribute [r] gems_fetcher
14
+ # @return [Gemirro::GemsFetcher]
15
+ #
16
+ class Server
17
+ attr_reader :server, :destination, :versions_fetcher, :gems_fetcher
18
+
19
+ ##
20
+ # Initialize Server
21
+ #
22
+ def initialize
23
+ configuration.server_host = 'localhost' if configuration.server_host.nil?
24
+ configuration.server_port = '2000' if configuration.server_port.nil?
25
+ logger.info('Running server on ' \
26
+ "#{configuration.server_host}:#{configuration.server_port}")
27
+ @server = TCPServer.new(
28
+ configuration.server_host,
29
+ configuration.server_port
30
+ )
31
+
32
+ @destination = configuration.destination
33
+ end
34
+
35
+ ##
36
+ # Run the server and accept all connection
37
+ #
38
+ # @return [nil]
39
+ #
40
+ def run
41
+ while (session = server.accept)
42
+ request = session.gets
43
+ logger.info(request)
44
+
45
+ trimmedrequest = request.gsub(/GET\ \//, '').gsub(/\ HTTP.*/, '').chomp
46
+ resource = "#{@destination}/#{trimmedrequest}"
47
+
48
+ # Try to download gem if file doesn't exists
49
+ fetch_gem(resource) unless File.exist?(resource)
50
+
51
+ # If not found again, return a 404
52
+ unless File.exist?(resource)
53
+ logger.warn("404 - #{trimmedrequest.gsub(/^public\//, '')}")
54
+ session.print "HTTP/1.1 404/Object Not Found\r\n\r\n"
55
+ session.close
56
+ next
57
+ end
58
+
59
+ if File.directory?(resource)
60
+ display_directory(session, resource)
61
+ else
62
+ mime_type = MIME::Types.type_for(resource)
63
+ session.print "HTTP/1.1 200/OK\r\nContent-type:#{mime_type}\r\n\r\n"
64
+ file = open(resource, 'rb')
65
+ session.puts(file.read)
66
+ end
67
+
68
+ session.close
69
+ end
70
+ end
71
+
72
+ ##
73
+ # Try to fetch gem and download its if it's possible, and
74
+ # build and install indicies.
75
+ #
76
+ # @param [String] resource
77
+ # @return [Indexer]
78
+ #
79
+ def fetch_gem(resource)
80
+ name = File.basename(resource)
81
+ regexp = /^(.*)-(\d+(?:\.\d+){,4})\.gem(?:spec\.rz)?$/
82
+ gem_name, gem_version = name.match(regexp).captures
83
+
84
+ return unless gem_name && gem_version
85
+
86
+ logger.info("Try to download #{gem_name} with version #{gem_version}")
87
+ begin
88
+ gems_fetcher.source.gems.clear
89
+ gems_fetcher.source.gems.push(Gemirro::Gem.new(gem_name, gem_version))
90
+ gems_fetcher.fetch
91
+ rescue StandardError => e
92
+ logger.error(e.message)
93
+ end
94
+
95
+ generate_index
96
+ end
97
+
98
+ ##
99
+ # Generate index and install indicies.
100
+ #
101
+ # @return [Indexer]
102
+ #
103
+ def generate_index
104
+ indexer = Indexer.new(configuration.destination)
105
+ indexer.ui = ::Gem::SilentUI.new
106
+
107
+ logger.info('Generating indexes')
108
+ indexer.generate_index
109
+ end
110
+
111
+ ##
112
+ # Display directory on the current sesion
113
+ #
114
+ # @param [TCPSocket] session
115
+ # @param [String] resource
116
+ # @return [Array]
117
+ #
118
+ def display_directory(session, resource)
119
+ session.print "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n"
120
+ base_dir = Dir.new(resource)
121
+ base_dir.entries.sort.each do |f|
122
+ dir_sign = ''
123
+ resource_path = resource.gsub(/\/$/, '') + '/' + f
124
+ dir_sign = '/' if File.directory?(resource_path)
125
+ resource_path = resource_path.gsub(/^public\//, '')
126
+ resource_path = resource_path.gsub(@destination, '')
127
+
128
+ session.print(
129
+ "<a href=\"#{resource_path}\">#{f}#{dir_sign}</a><br>"
130
+ ) unless ['.', '..'].include?(File.basename(resource_path))
131
+ end
132
+ end
133
+
134
+ ##
135
+ # @see Gemirro::Configuration#logger
136
+ # @return [Logger]
137
+ #
138
+ def logger
139
+ configuration.logger
140
+ end
141
+
142
+ ##
143
+ # @see Gemirro.configuration
144
+ #
145
+ def configuration
146
+ Gemirro.configuration
147
+ end
148
+
149
+ ##
150
+ # @see Gemirro::VersionsFetcher.fetch
151
+ #
152
+ def versions_fetcher
153
+ @versions_fetcher ||= Gemirro::VersionsFetcher.new(configuration.source).fetch
154
+ end
155
+
156
+ ##
157
+ # @return [Gemirro::GemsFetcher]
158
+ #
159
+ def gems_fetcher
160
+ @gems_fetcher = Gemirro::GemsFetcher.new(
161
+ configuration.source, versions_fetcher)
162
+ end
163
+ end
164
+ end