gemstash 1.0.0.pre.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +13 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +139 -0
  6. data/Rakefile +35 -0
  7. data/bin/console +14 -0
  8. data/bin/gemstash +3 -0
  9. data/bin/setup +5 -0
  10. data/docs/config.md +136 -0
  11. data/docs/debug.md +24 -0
  12. data/docs/deploy.md +30 -0
  13. data/docs/mirror.md +30 -0
  14. data/docs/multiple_sources.md +68 -0
  15. data/docs/private_gems.md +140 -0
  16. data/docs/reference.md +308 -0
  17. data/exe/gemstash +3 -0
  18. data/gemstash.gemspec +47 -0
  19. data/gemstash.png +0 -0
  20. data/lib/gemstash.rb +26 -0
  21. data/lib/gemstash/authorization.rb +87 -0
  22. data/lib/gemstash/cache.rb +79 -0
  23. data/lib/gemstash/cli.rb +71 -0
  24. data/lib/gemstash/cli/authorize.rb +69 -0
  25. data/lib/gemstash/cli/base.rb +46 -0
  26. data/lib/gemstash/cli/setup.rb +173 -0
  27. data/lib/gemstash/cli/start.rb +52 -0
  28. data/lib/gemstash/cli/status.rb +21 -0
  29. data/lib/gemstash/cli/stop.rb +21 -0
  30. data/lib/gemstash/config.ru +13 -0
  31. data/lib/gemstash/configuration.rb +41 -0
  32. data/lib/gemstash/db.rb +15 -0
  33. data/lib/gemstash/db/authorization.rb +20 -0
  34. data/lib/gemstash/db/dependency.rb +50 -0
  35. data/lib/gemstash/db/rubygem.rb +14 -0
  36. data/lib/gemstash/db/version.rb +51 -0
  37. data/lib/gemstash/dependencies.rb +93 -0
  38. data/lib/gemstash/env.rb +150 -0
  39. data/lib/gemstash/gem_fetcher.rb +50 -0
  40. data/lib/gemstash/gem_pusher.rb +125 -0
  41. data/lib/gemstash/gem_source.rb +37 -0
  42. data/lib/gemstash/gem_source/dependency_caching.rb +40 -0
  43. data/lib/gemstash/gem_source/private_source.rb +139 -0
  44. data/lib/gemstash/gem_source/rack_middleware.rb +22 -0
  45. data/lib/gemstash/gem_source/upstream_source.rb +183 -0
  46. data/lib/gemstash/gem_unyanker.rb +61 -0
  47. data/lib/gemstash/gem_yanker.rb +61 -0
  48. data/lib/gemstash/http_client.rb +77 -0
  49. data/lib/gemstash/logging.rb +93 -0
  50. data/lib/gemstash/migrations/01_gem_dependencies.rb +41 -0
  51. data/lib/gemstash/migrations/02_authorizations.rb +12 -0
  52. data/lib/gemstash/puma.rb +6 -0
  53. data/lib/gemstash/rack_env_rewriter.rb +66 -0
  54. data/lib/gemstash/specs_builder.rb +93 -0
  55. data/lib/gemstash/storage.rb +207 -0
  56. data/lib/gemstash/upstream.rb +65 -0
  57. data/lib/gemstash/version.rb +4 -0
  58. data/lib/gemstash/web.rb +97 -0
  59. metadata +306 -0
@@ -0,0 +1,61 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ # Class that supports unyanking a gem from the private repository of gems.
5
+ class GemUnyanker
6
+ include Gemstash::Env::Helper
7
+
8
+ # This error is thrown when unyanking a non-existing gem name.
9
+ class UnknownGemError < StandardError
10
+ end
11
+
12
+ # This error is thrown when unyanking a non-existing gem version.
13
+ class UnknownVersionError < StandardError
14
+ end
15
+
16
+ # This error is thrown when unyanking a non-yanked gem version.
17
+ class NotYankedVersionError < StandardError
18
+ end
19
+
20
+ def initialize(auth_key, gem_name, slug)
21
+ @auth_key = auth_key
22
+ @gem_name = gem_name
23
+ @slug = slug
24
+ end
25
+
26
+ def unyank
27
+ check_auth
28
+ update_database
29
+ invalidate_cache
30
+ end
31
+
32
+ private
33
+
34
+ def storage
35
+ @storage ||= Gemstash::Storage.for("private").for("gems")
36
+ end
37
+
38
+ def full_name
39
+ @full_name ||= "#{@gem_name}-#{@slug}"
40
+ end
41
+
42
+ def check_auth
43
+ Gemstash::Authorization.check(@auth_key, "unyank")
44
+ end
45
+
46
+ def update_database
47
+ gemstash_env.db.transaction do
48
+ raise UnknownGemError, "Cannot unyank an unknown gem!" unless Gemstash::DB::Rubygem[name: @gem_name]
49
+ version = Gemstash::DB::Version.find_by_full_name(full_name)
50
+ raise UnknownVersionError, "Cannot unyank an unknown version!" unless version
51
+ raise NotYankedVersionError, "Cannot unyank a non-yanked version!" if version.indexed
52
+ version.reindex
53
+ storage.resource(version.storage_id).update_properties(indexed: true)
54
+ end
55
+ end
56
+
57
+ def invalidate_cache
58
+ gemstash_env.cache.invalidate_gem("private", @gem_name)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ # Class that supports yanking a gem from the private repository of gems.
5
+ class GemYanker
6
+ include Gemstash::Env::Helper
7
+
8
+ # This error is thrown when yanking a non-existing gem name.
9
+ class UnknownGemError < StandardError
10
+ end
11
+
12
+ # This error is thrown when yanking a non-existing gem version.
13
+ class UnknownVersionError < StandardError
14
+ end
15
+
16
+ # This error is thrown when yanking an already yanked gem version.
17
+ class YankedVersionError < StandardError
18
+ end
19
+
20
+ def initialize(auth_key, gem_name, slug)
21
+ @auth_key = auth_key
22
+ @gem_name = gem_name
23
+ @slug = slug
24
+ end
25
+
26
+ def yank
27
+ check_auth
28
+ update_database
29
+ invalidate_cache
30
+ end
31
+
32
+ private
33
+
34
+ def storage
35
+ @storage ||= Gemstash::Storage.for("private").for("gems")
36
+ end
37
+
38
+ def full_name
39
+ @full_name ||= "#{@gem_name}-#{@slug}"
40
+ end
41
+
42
+ def check_auth
43
+ Gemstash::Authorization.check(@auth_key, "yank")
44
+ end
45
+
46
+ def update_database
47
+ gemstash_env.db.transaction do
48
+ raise UnknownGemError, "Cannot yank an unknown gem!" unless Gemstash::DB::Rubygem[name: @gem_name]
49
+ version = Gemstash::DB::Version.find_by_full_name(full_name)
50
+ raise UnknownVersionError, "Cannot yank an unknown version!" unless version
51
+ raise YankedVersionError, "Cannot yank an already yanked version!" unless version.indexed
52
+ version.deindex
53
+ storage.resource(version.storage_id).update_properties(indexed: false)
54
+ end
55
+ end
56
+
57
+ def invalidate_cache
58
+ gemstash_env.cache.invalidate_gem("private", @gem_name)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,77 @@
1
+ require "gemstash"
2
+ require "faraday"
3
+ require "faraday_middleware"
4
+
5
+ module Gemstash
6
+ #:nodoc:
7
+ class WebError < StandardError
8
+ attr_reader :code
9
+
10
+ def initialize(message, code)
11
+ @code = code
12
+ super(message)
13
+ end
14
+ end
15
+
16
+ #:nodoc:
17
+ class ConnectionError < WebError
18
+ def initialize(message)
19
+ super(message, 502) # Bad Gateway
20
+ end
21
+ end
22
+
23
+ #:nodoc:
24
+ class HTTPClient
25
+ include Gemstash::Logging
26
+
27
+ DEFAULT_USER_AGENT = "Gemstash/#{Gemstash::VERSION}"
28
+
29
+ def self.for(upstream)
30
+ client = Faraday.new(upstream.to_s) do |config|
31
+ config.use FaradayMiddleware::FollowRedirects
32
+ config.adapter :net_http
33
+ end
34
+ user_agent = "#{upstream.user_agent} " unless upstream.user_agent.to_s.empty?
35
+ user_agent = user_agent.to_s + DEFAULT_USER_AGENT
36
+
37
+ new(client, user_agent: user_agent)
38
+ end
39
+
40
+ def initialize(client = nil, user_agent: nil)
41
+ @client = client
42
+ @user_agent = user_agent || DEFAULT_USER_AGENT
43
+ end
44
+
45
+ def get(path)
46
+ response = with_retries do
47
+ @client.get(path) do |req|
48
+ req.headers["User-Agent"] = @user_agent
49
+ req.options.open_timeout = 2
50
+ end
51
+ end
52
+
53
+ raise Gemstash::WebError.new(response.body, response.status) unless response.success?
54
+
55
+ if block_given?
56
+ yield(response.body, response.headers)
57
+ else
58
+ response.body
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def with_retries(times: 3, &block)
65
+ loop do
66
+ times -= 1
67
+ begin
68
+ return block.call
69
+ rescue Faraday::ConnectionFailed => e
70
+ log_error("Connection failure", e)
71
+ raise(ConnectionError, e.message) unless times > 0
72
+ log.info "retrying... #{times} more times"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,93 @@
1
+ require "logger"
2
+ require "puma/events"
3
+
4
+ module Gemstash
5
+ #:nodoc:
6
+ module Logging
7
+ LEVELS = {
8
+ debug: Logger::DEBUG,
9
+ info: Logger::INFO,
10
+ warn: Logger::WARN,
11
+ error: Logger::ERROR,
12
+ fatal: Logger::FATAL
13
+ }
14
+
15
+ def log
16
+ Gemstash::Logging.logger
17
+ end
18
+
19
+ def log_error(message, error, level: :error)
20
+ log.add(LEVELS[level]) do
21
+ "#{message} - #{error.message} (#{error.class})\n #{error.backtrace.join("\n ")}"
22
+ end
23
+ end
24
+
25
+ def self.setup_logger(logfile)
26
+ @logger = Logger.new(logfile, 2, 10_485_760)
27
+ @logger.level = Logger::INFO
28
+ @logger.datetime_format = "%d/%b/%Y:%H:%M:%S %z"
29
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
30
+ if msg.end_with?("\n")
31
+ "[#{datetime}] - #{severity} - #{msg}"
32
+ else
33
+ "[#{datetime}] - #{severity} - #{msg}\n"
34
+ end
35
+ end
36
+ @logger
37
+ end
38
+
39
+ def self.logger
40
+ @logger ||= setup_logger($stdout)
41
+ end
42
+
43
+ def self.reset
44
+ @logger.close if @logger
45
+ @logger = nil
46
+ end
47
+
48
+ # Rack middleware to set the Rack logger to the Gemstash logger.
49
+ class RackMiddleware
50
+ def initialize(app)
51
+ @app = app
52
+ end
53
+
54
+ def call(env)
55
+ env["rack.logger"] = Gemstash::Logging.logger
56
+ @app.call(env)
57
+ end
58
+ end
59
+
60
+ # Logger that looks like a stream, for Puma and Rack to log to.
61
+ class StreamLogger
62
+ def self.puma_events
63
+ Puma::Events.new(for_stdout, for_stderr)
64
+ end
65
+
66
+ def self.for_stdout
67
+ new(Logger::INFO)
68
+ end
69
+
70
+ def self.for_stderr
71
+ new(Logger::ERROR)
72
+ end
73
+
74
+ def initialize(level)
75
+ @level = level
76
+ end
77
+
78
+ def flush
79
+ end
80
+
81
+ def sync=(_value)
82
+ end
83
+
84
+ def write(message)
85
+ Gemstash::Logging.logger.add(@level, message)
86
+ end
87
+
88
+ def puts(message)
89
+ Gemstash::Logging.logger.add(@level, message)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,41 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table :rubygems do
4
+ primary_key :id
5
+ String :name, :size => 255, :null => false
6
+ DateTime :created_at, :null => false
7
+ DateTime :updated_at, :null => false
8
+ index [:name], :unique => true
9
+ end
10
+
11
+ create_table :versions do
12
+ primary_key :id
13
+ Integer :rubygem_id, :null => false
14
+ String :storage_id, :size => 255, :null => false
15
+ String :number, :size => 255, :null => false
16
+ String :platform, :size => 255, :null => false
17
+ String :full_name, :size => 255, :null => false
18
+ TrueClass :indexed, :default => true, :null => false
19
+ TrueClass :prerelease, :null => false
20
+ DateTime :created_at, :null => false
21
+ DateTime :updated_at, :null => false
22
+ index [:rubygem_id, :number, :platform], :unique => true
23
+ index [:indexed]
24
+ index [:indexed, :prerelease]
25
+ index [:number]
26
+ index [:full_name], :unique => true
27
+ index [:storage_id], :unique => true
28
+ end
29
+
30
+ create_table :dependencies do
31
+ primary_key :id
32
+ Integer :version_id, :null => false
33
+ String :rubygem_name, :size => 255, :null => false
34
+ String :requirements, :size => 255, :null => false
35
+ DateTime :created_at, :null => false
36
+ DateTime :updated_at, :null => false
37
+ index [:version_id]
38
+ index [:rubygem_name]
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,12 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table :authorizations do
4
+ primary_key :id
5
+ String :auth_key, :size => 2056, :null => false
6
+ String :permissions, :size => 255, :null => false
7
+ DateTime :created_at, :null => false
8
+ DateTime :updated_at, :null => false
9
+ index [:auth_key], :unique => true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ require "gemstash"
2
+
3
+ threads 0, 16
4
+ bind "#{Gemstash::Env.current.config[:bind]}"
5
+ workers 1
6
+ rackup Gemstash::Env.current.rackup
@@ -0,0 +1,66 @@
1
+ require "gemstash"
2
+ require "forwardable"
3
+
4
+ module Gemstash
5
+ # Detects patterns in the Rack env variables related to URI and rewrites
6
+ # them, extracting parameters.
7
+ class RackEnvRewriter
8
+ attr_reader :regexp
9
+
10
+ def initialize(regexp)
11
+ @regexp = regexp
12
+ end
13
+
14
+ def for(rack_env)
15
+ Context.new(self, rack_env)
16
+ end
17
+
18
+ # Context containing the logic and the actual Rack environment.
19
+ class Context
20
+ include Gemstash::Logging
21
+ extend Forwardable
22
+ def_delegators :@rewriter, :regexp
23
+
24
+ def initialize(rewriter, rack_env)
25
+ @rewriter = rewriter
26
+ @rack_env = rack_env
27
+ end
28
+
29
+ def matches?
30
+ matches_request_uri? && matches_path_info?
31
+ end
32
+
33
+ def rewrite
34
+ check_match
35
+ log_start = "Rewriting '#{@rack_env["REQUEST_URI"]}'"
36
+ @rack_env["REQUEST_URI"][@request_uri_match.begin(0)...@request_uri_match.end(0)] = ""
37
+ @rack_env["PATH_INFO"][@path_info_match.begin(0)...@path_info_match.end(0)] = ""
38
+ log.info "#{log_start} to '#{@rack_env["REQUEST_URI"]}'"
39
+ end
40
+
41
+ def captures
42
+ @params ||= begin
43
+ check_match
44
+ @path_info_match.names.inject({}) do |result, name|
45
+ result[name] = @path_info_match[name]
46
+ result
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def matches_request_uri?
54
+ @request_uri_match ||= @rack_env["REQUEST_URI"].match(regexp)
55
+ end
56
+
57
+ def matches_path_info?
58
+ @path_info_match ||= @rack_env["PATH_INFO"].match(regexp)
59
+ end
60
+
61
+ def check_match
62
+ raise "Rack env did not match!" unless @request_uri_match && @path_info_match
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,93 @@
1
+ require "gemstash"
2
+ require "stringio"
3
+ require "zlib"
4
+
5
+ module Gemstash
6
+ # Builds a Marshal'ed and GZipped array of arrays containing specs as:
7
+ # [name, Gem::Version, platform]
8
+ class SpecsBuilder
9
+ attr_reader :result
10
+
11
+ # Used for the /private/specs.4.8.gz endpoint. Fetches non-prerelease,
12
+ # indexed private gems.
13
+ def self.all
14
+ new.build
15
+ end
16
+
17
+ # Used for the /private/prerelease_specs.4.8.gz endpoint. Fetches
18
+ # prerelease, indexed private gems.
19
+ def self.prerelease
20
+ new(prerelease: true).build
21
+ end
22
+
23
+ def self.invalidate_stored
24
+ storage = Gemstash::Storage.for("private").for("specs_collection")
25
+ storage.resource("specs.4.8.gz").delete(:specs)
26
+ storage.resource("prerelease_specs.4.8.gz").delete(:specs)
27
+ end
28
+
29
+ def initialize(prerelease: false)
30
+ @prerelease = prerelease
31
+ end
32
+
33
+ def build
34
+ fetch_from_storage
35
+ return result if result
36
+ fetch_versions
37
+ marshal
38
+ gzip
39
+ store_result
40
+ result
41
+ end
42
+
43
+ private
44
+
45
+ def storage
46
+ @storage ||= Gemstash::Storage.for("private").for("specs_collection")
47
+ end
48
+
49
+ def fetch_resource
50
+ if @prerelease
51
+ storage.resource("prerelease_specs.4.8.gz")
52
+ else
53
+ storage.resource("specs.4.8.gz")
54
+ end
55
+ end
56
+
57
+ def fetch_from_storage
58
+ specs = fetch_resource
59
+ return unless specs.exist?(:specs)
60
+ @result = specs.load(:specs).content(:specs)
61
+ rescue
62
+ # On the off-chance of a race condition between specs.exist? and specs.load
63
+ @result = nil
64
+ end
65
+
66
+ def fetch_versions
67
+ @versions = Gemstash::DB::Version.for_spec_collection(prerelease: @prerelease).map(&:to_spec)
68
+ end
69
+
70
+ def marshal
71
+ @marshal ||= Marshal.dump(@versions)
72
+ end
73
+
74
+ def gzip
75
+ @result ||= begin
76
+ output = StringIO.new
77
+ gz = Zlib::GzipWriter.new(output)
78
+
79
+ begin
80
+ gz.write(@marshal)
81
+ ensure
82
+ gz.close
83
+ end
84
+
85
+ output.string
86
+ end
87
+ end
88
+
89
+ def store_result
90
+ fetch_resource.save(specs: @result)
91
+ end
92
+ end
93
+ end