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,50 @@
1
+ require "gemstash"
2
+ require "set"
3
+
4
+ module Gemstash
5
+ #:nodoc:
6
+ class GemFetcher
7
+ def initialize(http_client)
8
+ @http_client = http_client
9
+ @valid_headers = Set.new(["etag", "content-type", "content-length", "last-modified"])
10
+ end
11
+
12
+ def fetch(gem_id, type, &block)
13
+ @http_client.get(path_for(gem_id, type)) do |body, headers|
14
+ properties = filter_headers(headers)
15
+ validate_download(body, properties)
16
+ yield body, properties
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def path_for(gem_id, type)
23
+ case type
24
+ when :gem
25
+ "gems/#{gem_id}"
26
+ when :spec
27
+ "quick/Marshal.4.8/#{gem_id}"
28
+ else
29
+ raise "Invalid type #{type.inspect}"
30
+ end
31
+ end
32
+
33
+ def filter_headers(headers)
34
+ headers.inject({}) do|properties, (key, value)|
35
+ properties[key.downcase] = value if @valid_headers.include?(key.downcase)
36
+ properties
37
+ end
38
+ end
39
+
40
+ def validate_download(content, headers)
41
+ expected_size = content_length(headers)
42
+ raise "Incomplete download, only #{body.length} was downloaded out of #{expected_size}" \
43
+ if content.length < expected_size
44
+ end
45
+
46
+ def content_length(headers)
47
+ headers["content-length"].to_i
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,125 @@
1
+ require "gemstash"
2
+ require "rubygems/package"
3
+ require "stringio"
4
+
5
+ #:nodoc:
6
+ module Gemstash
7
+ # Class that supports pushing a new gem to the private repository of gems.
8
+ class GemPusher
9
+ include Gemstash::Env::Helper
10
+
11
+ # This error is thrown when pushing to an existing version.
12
+ class ExistingVersionError < StandardError
13
+ end
14
+
15
+ # This error is thrown when pushing to a yanked version.
16
+ class YankedVersionError < ExistingVersionError
17
+ end
18
+
19
+ def initialize(auth_key, content)
20
+ @auth_key = auth_key
21
+ @content = content
22
+ end
23
+
24
+ def push
25
+ check_auth
26
+ store_gem
27
+ store_gemspec
28
+ save_to_database
29
+ invalidate_cache
30
+ end
31
+
32
+ private
33
+
34
+ def gem
35
+ @gem ||= Gem::Package.new(StringIO.new(@content))
36
+ end
37
+
38
+ def storage
39
+ @storage ||= Gemstash::Storage.for("private").for("gems")
40
+ end
41
+
42
+ def full_name
43
+ @full_name ||= gem.spec.full_name
44
+ end
45
+
46
+ def check_auth
47
+ Gemstash::Authorization.check(@auth_key, "push")
48
+ end
49
+
50
+ def store_gem
51
+ storage.resource(full_name).save({ gem: @content }, indexed: true)
52
+ end
53
+
54
+ def store_gemspec
55
+ spec = gem.spec
56
+ spec = Marshal.dump(spec)
57
+ spec = Zlib::Deflate.deflate(spec)
58
+ storage.resource(full_name).save(spec: spec)
59
+ end
60
+
61
+ def save_to_database
62
+ spec = gem.spec
63
+
64
+ gemstash_env.db.transaction do
65
+ gem_id = Gemstash::DB::Rubygem.find_or_insert(spec)
66
+ existing = Gemstash::DB::Version.find_by_spec(gem_id, spec)
67
+
68
+ if existing
69
+ if existing.indexed
70
+ raise ExistingVersionError, "Cannot push to an existing version!"
71
+ else
72
+ raise YankedVersionError, "Cannot push to a yanked version!"
73
+ end
74
+ end
75
+
76
+ version_id = Gemstash::DB::Version.insert_by_spec(gem_id, spec)
77
+ Gemstash::DB::Dependency.insert_by_spec(version_id, spec)
78
+ end
79
+ end
80
+
81
+ def invalidate_cache
82
+ gemstash_env.cache.invalidate_gem("private", gem.spec.name)
83
+ end
84
+ end
85
+
86
+ unless Gem::Requirement.new(">= 2.4").satisfied_by?(Gem::Version.new(Gem::VERSION))
87
+ require "tempfile"
88
+
89
+ # Adds support for legacy versions of RubyGems
90
+ module LegacyRubyGemsSupport
91
+ def self.included(base)
92
+ base.class_eval do
93
+ alias_method :push_without_cleanup, :push
94
+ remove_method :push
95
+ remove_method :gem
96
+ end
97
+ end
98
+
99
+ def push
100
+ push_without_cleanup
101
+ ensure
102
+ cleanup
103
+ end
104
+
105
+ private
106
+
107
+ def gem
108
+ @gem ||= begin
109
+ @tempfile = Tempfile.new("gemstash-gem")
110
+ @tempfile.write(@content)
111
+ @tempfile.flush
112
+ Gem::Package.new(@tempfile.path)
113
+ end
114
+ end
115
+
116
+ def cleanup
117
+ return unless @tempfile
118
+ @tempfile.close
119
+ @tempfile.unlink
120
+ end
121
+ end
122
+
123
+ GemPusher.send(:include, LegacyRubyGemsSupport)
124
+ end
125
+ end
@@ -0,0 +1,37 @@
1
+ require "gemstash"
2
+ require "forwardable"
3
+
4
+ module Gemstash
5
+ #:nodoc:
6
+ module GemSource
7
+ autoload :DependencyCaching, "gemstash/gem_source/dependency_caching"
8
+ autoload :PrivateSource, "gemstash/gem_source/private_source"
9
+ autoload :RackMiddleware, "gemstash/gem_source/rack_middleware"
10
+ autoload :RedirectSource, "gemstash/gem_source/upstream_source"
11
+ autoload :RubygemsSource, "gemstash/gem_source/upstream_source"
12
+ autoload :UpstreamSource, "gemstash/gem_source/upstream_source"
13
+
14
+ def self.sources
15
+ @sources ||= [
16
+ Gemstash::GemSource::PrivateSource,
17
+ Gemstash::GemSource::RedirectSource,
18
+ Gemstash::GemSource::UpstreamSource,
19
+ Gemstash::GemSource::RubygemsSource
20
+ ]
21
+ end
22
+
23
+ # Base GemSource for some common utilities.
24
+ class Base
25
+ extend Forwardable
26
+ extend Gemstash::Logging
27
+ include Gemstash::Logging
28
+
29
+ def_delegators :@app, :cache_control, :content_type, :env, :halt,
30
+ :headers, :http_client_for, :params, :redirect, :request
31
+
32
+ def initialize(app)
33
+ @app = app
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ module Gemstash
2
+ module GemSource
3
+ # Module for caching dependencies in a GemSource.
4
+ module DependencyCaching
5
+ API_REQUEST_LIMIT = 200
6
+
7
+ def serve_dependencies
8
+ gems = gems_from_params
9
+
10
+ if gems.length > API_REQUEST_LIMIT
11
+ halt 422, "Too many gems (use --full-index instead)"
12
+ end
13
+
14
+ content_type "application/octet-stream"
15
+ Marshal.dump dependencies.fetch(gems)
16
+ end
17
+
18
+ def serve_dependencies_json
19
+ gems = gems_from_params
20
+
21
+ if gems.length > API_REQUEST_LIMIT
22
+ halt 422, {
23
+ "error" => "Too many gems (use --full-index instead)",
24
+ "code" => 422
25
+ }.to_json
26
+ end
27
+
28
+ content_type "application/json;charset=UTF-8"
29
+ dependencies.fetch(gems).to_json
30
+ end
31
+
32
+ private
33
+
34
+ def gems_from_params
35
+ halt(200) if params[:gems].nil? || params[:gems].empty?
36
+ params[:gems].split(",").uniq
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,139 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ module GemSource
5
+ # GemSource for privately stored gems.
6
+ class PrivateSource < Gemstash::GemSource::Base
7
+ include Gemstash::GemSource::DependencyCaching
8
+ include Gemstash::Env::Helper
9
+
10
+ def self.rack_env_rewriter
11
+ @rack_env_rewriter ||= Gemstash::RackEnvRewriter.new(%r{\A/private})
12
+ end
13
+
14
+ def self.matches?(env)
15
+ rewriter = rack_env_rewriter.for(env)
16
+ return false unless rewriter.matches?
17
+ rewriter.rewrite
18
+ true
19
+ end
20
+
21
+ def serve_root
22
+ halt 403, "Not yet supported"
23
+ end
24
+
25
+ def serve_add_gem
26
+ authenticated("Gemstash Private Gems") do
27
+ auth = request.env["HTTP_AUTHORIZATION"]
28
+ gem = request.body.read
29
+ Gemstash::GemPusher.new(auth, gem).push
30
+ end
31
+ end
32
+
33
+ def serve_yank
34
+ authenticated("Gemstash Private Gems") do
35
+ auth = request.env["HTTP_AUTHORIZATION"]
36
+ gem_name = params[:gem_name]
37
+ Gemstash::GemYanker.new(auth, gem_name, slug_param).yank
38
+ end
39
+ end
40
+
41
+ def serve_unyank
42
+ authenticated("Gemstash Private Gems") do
43
+ auth = request.env["HTTP_AUTHORIZATION"]
44
+ gem_name = params[:gem_name]
45
+ Gemstash::GemUnyanker.new(auth, gem_name, slug_param).unyank
46
+ end
47
+ end
48
+
49
+ def serve_add_spec_json
50
+ halt 403, "Not yet supported"
51
+ end
52
+
53
+ def serve_remove_spec_json
54
+ halt 403, "Not yet supported"
55
+ end
56
+
57
+ def serve_names
58
+ halt 403, "Not yet supported"
59
+ end
60
+
61
+ def serve_versions
62
+ halt 403, "Not yet supported"
63
+ end
64
+
65
+ def serve_info(name)
66
+ halt 403, "Not yet supported"
67
+ end
68
+
69
+ def serve_marshal(id)
70
+ gem_full_name = id.sub(/\.gemspec\.rz\z/, "")
71
+ gem = fetch_gem(gem_full_name)
72
+ halt 404 unless gem.exist?(:spec)
73
+ content_type "application/octet-stream"
74
+ gem.load(:spec).content(:spec)
75
+ end
76
+
77
+ def serve_actual_gem(id)
78
+ halt 403, "Not yet supported"
79
+ end
80
+
81
+ def serve_gem(id)
82
+ gem_full_name = id.sub(/\.gem\z/, "")
83
+ gem = fetch_gem(gem_full_name)
84
+ content_type "application/octet-stream"
85
+ gem.content(:gem)
86
+ end
87
+
88
+ def serve_latest_specs
89
+ halt 403, "Not yet supported"
90
+ end
91
+
92
+ def serve_specs
93
+ content_type "application/octet-stream"
94
+ Gemstash::SpecsBuilder.all
95
+ end
96
+
97
+ def serve_prerelease_specs
98
+ content_type "application/octet-stream"
99
+ Gemstash::SpecsBuilder.prerelease
100
+ end
101
+
102
+ private
103
+
104
+ def slug_param
105
+ version = params[:version]
106
+ platform = params[:platform]
107
+
108
+ if platform.to_s.empty?
109
+ version
110
+ else
111
+ "#{version}-#{platform}"
112
+ end
113
+ end
114
+
115
+ def authenticated(realm)
116
+ yield
117
+ rescue Gemstash::NotAuthorizedError => e
118
+ headers["WWW-Authenticate"] = "Basic realm=\"#{realm}\""
119
+ halt 401, e.message
120
+ end
121
+
122
+ def dependencies
123
+ @dependencies ||= Gemstash::Dependencies.for_private
124
+ end
125
+
126
+ def storage
127
+ @storage ||= Gemstash::Storage.for("private").for("gems")
128
+ end
129
+
130
+ def fetch_gem(gem_full_name)
131
+ gem = storage.resource(gem_full_name)
132
+ halt 404 unless gem.exist?(:gem)
133
+ gem.load(:gem)
134
+ halt 403, "That gem has been yanked" unless gem.properties[:indexed]
135
+ gem
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,22 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ module GemSource
5
+ # Rack middleware to detect the gem source from the URL.
6
+ class RackMiddleware
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ Gemstash::GemSource.sources.each do |source|
13
+ next unless source.matches?(env)
14
+ env["gemstash.gem_source"] = source
15
+ break
16
+ end
17
+
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,183 @@
1
+ require "gemstash"
2
+ require "cgi"
3
+
4
+ module Gemstash
5
+ module GemSource
6
+ # GemSource that purely redirects to the upstream server.
7
+ class RedirectSource < Gemstash::GemSource::Base
8
+ def self.rack_env_rewriter
9
+ @rack_env_rewriter ||= Gemstash::RackEnvRewriter.new(%r{\A/redirect/(?<upstream_url>[^/]+)})
10
+ end
11
+
12
+ def self.matches?(env)
13
+ rewriter = rack_env_rewriter.for(env)
14
+ return false unless rewriter.matches?
15
+ rewriter.rewrite
16
+ env["gemstash.upstream"] = rewriter.captures["upstream_url"]
17
+ capture_user_agent(env)
18
+ true
19
+ end
20
+
21
+ def self.capture_user_agent(env)
22
+ env["gemstash.user-agent"] = env["HTTP_USER_AGENT"]
23
+ end
24
+
25
+ def serve_root
26
+ cache_control :public, :max_age => 31_536_000
27
+ redirect upstream.url(nil, request.query_string)
28
+ end
29
+
30
+ def serve_add_gem
31
+ halt 403, "Cannot add gem to an upstream server!"
32
+ end
33
+
34
+ def serve_yank
35
+ halt 403, "Cannot yank from an upstream server!"
36
+ end
37
+
38
+ def serve_unyank
39
+ halt 403, "Cannot unyank from an upstream server!"
40
+ end
41
+
42
+ def serve_add_spec_json
43
+ halt 403, "Cannot add spec to an upstream server!"
44
+ end
45
+
46
+ def serve_remove_spec_json
47
+ halt 403, "Cannot remove spec from an upstream server!"
48
+ end
49
+
50
+ def serve_dependencies
51
+ redirect upstream.url("api/v1/dependencies", request.query_string)
52
+ end
53
+
54
+ def serve_dependencies_json
55
+ redirect upstream.url("api/v1/dependencies.json", request.query_string)
56
+ end
57
+
58
+ def serve_names
59
+ redirect upstream.url("names", request.query_string)
60
+ end
61
+
62
+ def serve_versions
63
+ redirect upstream.url("versions", request.query_string)
64
+ end
65
+
66
+ def serve_info(name)
67
+ redirect upstream.url("info/#{name}", request.query_string)
68
+ end
69
+
70
+ def serve_marshal(id)
71
+ redirect upstream.url("quick/Marshal.4.8/#{id}", request.query_string)
72
+ end
73
+
74
+ def serve_actual_gem(id)
75
+ redirect upstream.url("fetch/actual/gem/#{id}", request.query_string)
76
+ end
77
+
78
+ def serve_gem(id)
79
+ redirect upstream.url("gems/#{id}", request.query_string)
80
+ end
81
+
82
+ def serve_latest_specs
83
+ redirect upstream.url("latest_specs.4.8.gz", request.query_string)
84
+ end
85
+
86
+ def serve_specs
87
+ redirect upstream.url("specs.4.8.gz", request.query_string)
88
+ end
89
+
90
+ def serve_prerelease_specs
91
+ redirect upstream.url("prerelease_specs.4.8.gz", request.query_string)
92
+ end
93
+
94
+ private
95
+
96
+ def upstream
97
+ @upstream ||= Gemstash::Upstream.new(env["gemstash.upstream"],
98
+ user_agent: env["gemstash.user-agent"])
99
+ end
100
+ end
101
+
102
+ # GemSource for gems in an upstream server.
103
+ class UpstreamSource < Gemstash::GemSource::RedirectSource
104
+ include Gemstash::GemSource::DependencyCaching
105
+ include Gemstash::Env::Helper
106
+
107
+ def self.rack_env_rewriter
108
+ @rack_env_rewriter ||= Gemstash::RackEnvRewriter.new(%r{\A/upstream/(?<upstream_url>[^/]+)})
109
+ end
110
+
111
+ def serve_marshal(id)
112
+ serve_cached(id, :spec)
113
+ end
114
+
115
+ def serve_gem(id)
116
+ serve_cached(id, :gem)
117
+ end
118
+
119
+ private
120
+
121
+ def serve_cached(id, key)
122
+ gem = fetch_gem(id, key)
123
+ headers.update(gem.properties[:headers][key]) if gem.properties[:headers] && gem.properties[:headers][key]
124
+ gem.content(key)
125
+ rescue Gemstash::WebError => e
126
+ halt e.code
127
+ end
128
+
129
+ def dependencies
130
+ @dependencies ||= begin
131
+ http_client = http_client_for(upstream)
132
+ Gemstash::Dependencies.for_upstream(upstream, http_client)
133
+ end
134
+ end
135
+
136
+ def storage
137
+ @storage ||= Gemstash::Storage.for("gem_cache")
138
+ @storage.for(upstream.host_id)
139
+ end
140
+
141
+ def gem_fetcher
142
+ @gem_fetcher ||= Gemstash::GemFetcher.new(http_client_for(upstream))
143
+ end
144
+
145
+ def fetch_gem(id, key)
146
+ gem_name = Gemstash::Upstream::GemName.new(upstream, id)
147
+ gem_resource = storage.resource(gem_name.name)
148
+ if gem_resource.exist?(key)
149
+ fetch_local_gem(gem_name, gem_resource, key)
150
+ else
151
+ fetch_remote_gem(gem_name, gem_resource, key)
152
+ end
153
+ end
154
+
155
+ def fetch_local_gem(gem_name, gem_resource, key)
156
+ log.info "Gem #{gem_name.name} exists, returning cached #{key}"
157
+ gem_resource.load(key)
158
+ end
159
+
160
+ def fetch_remote_gem(gem_name, gem_resource, key)
161
+ log.info "Gem #{gem_name.name} is not cached, fetching #{key}"
162
+ gem_fetcher.fetch(gem_name.id, key) do |content, properties|
163
+ gem_resource.save({ key => content }, headers: { key => properties })
164
+ end
165
+ end
166
+ end
167
+
168
+ # GemSource for https://rubygems.org (specifically when defined by using the
169
+ # default upstream).
170
+ class RubygemsSource < Gemstash::GemSource::UpstreamSource
171
+ def self.matches?(env)
172
+ if env["HTTP_X_GEMFILE_SOURCE"].to_s.empty?
173
+ env["gemstash.upstream"] = env["gemstash.env"].config[:rubygems_url]
174
+ else
175
+ env["gemstash.upstream"] = env["HTTP_X_GEMFILE_SOURCE"]
176
+ end
177
+ capture_user_agent(env)
178
+
179
+ true
180
+ end
181
+ end
182
+ end
183
+ end