gemstash 1.0.0.pre.1

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