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,52 @@
1
+ require "gemstash"
2
+ require "puma/cli"
3
+
4
+ module Gemstash
5
+ class CLI
6
+ # This implements the command line start task to start the Gemstash server:
7
+ # $ gemstash start
8
+ class Start < Gemstash::CLI::Base
9
+ def run
10
+ prepare
11
+ setup_logging
12
+ store_daemonized
13
+ Puma::CLI.new(args, Gemstash::Logging::StreamLogger.puma_events).run
14
+ end
15
+
16
+ private
17
+
18
+ def setup_logging
19
+ return unless daemonize?
20
+ Gemstash::Logging.setup_logger(gemstash_env.base_file("server.log"))
21
+ end
22
+
23
+ def store_daemonized
24
+ Gemstash::Env.daemonized = daemonize?
25
+ end
26
+
27
+ def daemonize?
28
+ @cli.options[:daemonize]
29
+ end
30
+
31
+ def puma_config
32
+ File.expand_path("../../puma.rb", __FILE__)
33
+ end
34
+
35
+ def args
36
+ config_args + pidfile_args + daemonize_args
37
+ end
38
+
39
+ def config_args
40
+ ["--config", puma_config]
41
+ end
42
+
43
+ def daemonize_args
44
+ if daemonize?
45
+ ["--daemon"]
46
+ else
47
+ []
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ require "gemstash"
2
+ require "puma/control_cli"
3
+
4
+ module Gemstash
5
+ class CLI
6
+ # This implements the command line status task to check the server status:
7
+ # $ gemstash status
8
+ class Status < Gemstash::CLI::Base
9
+ def run
10
+ prepare
11
+ Puma::ControlCLI.new(args).run
12
+ end
13
+
14
+ private
15
+
16
+ def args
17
+ pidfile_args + %w(status)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require "gemstash"
2
+ require "puma/control_cli"
3
+
4
+ module Gemstash
5
+ class CLI
6
+ # This implements the command line stop task to stop the Gemstash server:
7
+ # $ gemstash stop
8
+ class Stop < Gemstash::CLI::Base
9
+ def run
10
+ prepare
11
+ Puma::ControlCLI.new(args).run
12
+ end
13
+
14
+ private
15
+
16
+ def args
17
+ pidfile_args + %w(stop)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ require "gemstash"
2
+ require "puma/commonlogger"
3
+
4
+ use Rack::Deflater
5
+ use Gemstash::Logging::RackMiddleware
6
+
7
+ if Gemstash::Env.daemonized?
8
+ use Puma::CommonLogger, Gemstash::Logging::StreamLogger.for_stdout
9
+ end
10
+
11
+ use Gemstash::Env::RackMiddleware, Gemstash::Env.current
12
+ use Gemstash::GemSource::RackMiddleware
13
+ run Gemstash::Web.new(gemstash_env: Gemstash::Env.current)
@@ -0,0 +1,41 @@
1
+ require "yaml"
2
+
3
+ module Gemstash
4
+ #:nodoc:
5
+ class Configuration
6
+ DEFAULTS = {
7
+ :cache_type => "memory",
8
+ :base_path => File.expand_path("~/.gemstash"),
9
+ :db_adapter => "sqlite3",
10
+ :bind => "tcp://0.0.0.0:9292",
11
+ :rubygems_url => "https://www.rubygems.org"
12
+ }.freeze
13
+
14
+ DEFAULT_FILE = File.expand_path("~/.gemstash/config.yml").freeze
15
+
16
+ def initialize(file: nil, config: nil)
17
+ if config
18
+ @config = DEFAULTS.merge(config).freeze
19
+ return
20
+ end
21
+
22
+ file ||= DEFAULT_FILE
23
+
24
+ if File.exist?(file)
25
+ @config = YAML.load_file(file)
26
+ @config = DEFAULTS.merge(@config)
27
+ @config.freeze
28
+ else
29
+ @config = DEFAULTS
30
+ end
31
+ end
32
+
33
+ def default?(key)
34
+ @config[key] == DEFAULTS[key]
35
+ end
36
+
37
+ def [](key)
38
+ @config[key]
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ # Module containing the DB models.
5
+ module DB
6
+ raise "Gemstash::DB cannot be loaded until the Gemstash::Env is available" unless Gemstash::Env.available?
7
+ Sequel::Model.db = Gemstash::Env.current.db
8
+ Sequel::Model.raise_on_save_failure = true
9
+ Sequel::Model.plugin :timestamps, update_on_create: true
10
+ autoload :Authorization, "gemstash/db/authorization"
11
+ autoload :Dependency, "gemstash/db/dependency"
12
+ autoload :Rubygem, "gemstash/db/rubygem"
13
+ autoload :Version, "gemstash/db/version"
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ module DB
5
+ # Sequel model for authorizations table.
6
+ class Authorization < Sequel::Model
7
+ def self.insert_or_update(auth_key, permissions)
8
+ db.transaction do
9
+ record = self[auth_key: auth_key]
10
+
11
+ if record
12
+ record.update(permissions: permissions)
13
+ else
14
+ create(auth_key: auth_key, permissions: permissions)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,50 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ module DB
5
+ # Sequel model for dependencies table.
6
+ class Dependency < Sequel::Model
7
+ def self.insert_by_spec(version_id, spec)
8
+ spec.runtime_dependencies.each do |dep|
9
+ requirements = dep.requirement.requirements
10
+ requirements = requirements.map {|r| "#{r.first} #{r.last}" }
11
+ requirements = requirements.join(", ")
12
+ create(version_id: version_id,
13
+ rubygem_name: dep.name,
14
+ requirements: requirements)
15
+ end
16
+ end
17
+
18
+ def self.fetch(gems)
19
+ results = db["
20
+ SELECT rubygem.name,
21
+ version.number, version.platform,
22
+ dependency.rubygem_name, dependency.requirements
23
+ FROM rubygems rubygem
24
+ JOIN versions version
25
+ ON version.rubygem_id = rubygem.id
26
+ LEFT JOIN dependencies dependency
27
+ ON dependency.version_id = version.id
28
+ WHERE rubygem.name IN ?
29
+ AND version.indexed = ?", gems.to_a, true].to_a
30
+ results.group_by {|r| r[:name] }.each do |gem, rows|
31
+ requirements = rows.group_by {|r| [r[:number], r[:platform]] }
32
+
33
+ value = requirements.map do |version, r|
34
+ deps = r.map {|x| [x[:rubygem_name], x[:requirements]] }
35
+ deps = [] if deps.size == 1 && deps.first.first.nil?
36
+
37
+ {
38
+ :name => gem,
39
+ :number => version.first,
40
+ :platform => version.last,
41
+ :dependencies => deps
42
+ }
43
+ end
44
+
45
+ yield(gem, value)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,14 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ module DB
5
+ # Sequel model for rubygems table.
6
+ class Rubygem < Sequel::Model
7
+ def self.find_or_insert(spec)
8
+ record = self[name: spec.name]
9
+ return record.id if record
10
+ new(name: spec.name).tap(&:save).id
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ require "gemstash"
2
+
3
+ module Gemstash
4
+ module DB
5
+ # Sequel model for versions table.
6
+ class Version < Sequel::Model
7
+ many_to_one :rubygem
8
+
9
+ def deindex
10
+ update(indexed: false)
11
+ end
12
+
13
+ def reindex
14
+ update(indexed: true)
15
+ end
16
+
17
+ # This converts to the format used by /private/specs.4.8.gz
18
+ def to_spec
19
+ [rubygem.name, Gem::Version.new(number), platform]
20
+ end
21
+
22
+ def self.for_spec_collection(prerelease: false)
23
+ where(indexed: true, prerelease: prerelease).association_join(:rubygem)
24
+ end
25
+
26
+ def self.find_by_spec(gem_id, spec)
27
+ self[rubygem_id: gem_id,
28
+ number: spec.version.to_s,
29
+ platform: spec.platform.to_s]
30
+ end
31
+
32
+ def self.find_by_full_name(full_name)
33
+ result = self[full_name: full_name]
34
+ return result if result
35
+ # Try again with the default platform, in case it is implied
36
+ self[full_name: "#{full_name}-ruby"]
37
+ end
38
+
39
+ def self.insert_by_spec(gem_id, spec)
40
+ gem_name = Gemstash::DB::Rubygem[gem_id].name
41
+ new(rubygem_id: gem_id,
42
+ number: spec.version.to_s,
43
+ platform: spec.platform.to_s,
44
+ full_name: "#{gem_name}-#{spec.version}-#{spec.platform}",
45
+ storage_id: spec.full_name,
46
+ indexed: true,
47
+ prerelease: spec.version.prerelease?).tap(&:save).id
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,93 @@
1
+ require "cgi"
2
+ require "set"
3
+
4
+ module Gemstash
5
+ #:nodoc:
6
+ class Dependencies
7
+ def self.for_private
8
+ new(scope: "private", db_model: Gemstash::DB::Dependency)
9
+ end
10
+
11
+ def self.for_upstream(upstream, http_client)
12
+ new(scope: "upstream/#{upstream}", http_client: http_client)
13
+ end
14
+
15
+ def initialize(scope: nil, http_client: nil, db_model: nil)
16
+ @scope = scope
17
+ @http_client = http_client
18
+ @db_model = db_model
19
+ end
20
+
21
+ def fetch(gems)
22
+ Fetcher.new(gems, @scope, @http_client, @db_model).fetch
23
+ end
24
+
25
+ #:nodoc:
26
+ class Fetcher
27
+ include Gemstash::Env::Helper
28
+ include Gemstash::Logging
29
+
30
+ def initialize(gems, scope, http_client, db_model)
31
+ @gems = Set.new(gems)
32
+ @scope = scope
33
+ @http_client = http_client
34
+ @db_model = db_model
35
+ @dependencies = []
36
+ end
37
+
38
+ def fetch
39
+ fetch_from_cache
40
+ fetch_from_database
41
+ fetch_from_web
42
+ cache_missing
43
+ @dependencies
44
+ end
45
+
46
+ private
47
+
48
+ def done?
49
+ @gems.empty?
50
+ end
51
+
52
+ def fetch_from_cache
53
+ gemstash_env.cache.dependencies(@scope, @gems) do |gem, value|
54
+ @gems.delete(gem)
55
+ @dependencies += value
56
+ end
57
+ end
58
+
59
+ def fetch_from_database
60
+ return if done?
61
+ return unless @db_model
62
+ log.info "Querying dependencies: #{@gems.to_a.join(", ")}"
63
+
64
+ @db_model.fetch(@gems) do |gem, value|
65
+ @gems.delete(gem)
66
+ gemstash_env.cache.set_dependency(@scope, gem, value)
67
+ @dependencies += value
68
+ end
69
+ end
70
+
71
+ def fetch_from_web
72
+ return if done?
73
+ return unless @http_client
74
+ log.info "Fetching dependencies: #{@gems.to_a.join(", ")}"
75
+ gems_param = @gems.map {|gem| CGI.escape(gem) }.join(",")
76
+ fetched = @http_client.get("api/v1/dependencies?gems=#{gems_param}")
77
+ fetched = Marshal.load(fetched).group_by {|r| r[:name] }
78
+
79
+ fetched.each do |gem, result|
80
+ @gems.delete(gem)
81
+ gemstash_env.cache.set_dependency(@scope, gem, result)
82
+ @dependencies += result
83
+ end
84
+ end
85
+
86
+ def cache_missing
87
+ @gems.each do |gem|
88
+ gemstash_env.cache.set_dependency(@scope, gem, [])
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,150 @@
1
+ require "gemstash"
2
+ require "dalli"
3
+ require "fileutils"
4
+ require "sequel"
5
+ require "uri"
6
+
7
+ module Gemstash
8
+ # Storage for application-wide variables and configuration.
9
+ class Env
10
+ # The Gemstash::Env must be set before being retreived via
11
+ # Gemstash::Env.current. This error is thrown when that is not honored.
12
+ class EnvNotSetError < StandardError
13
+ end
14
+
15
+ # Little module to provide easy access to the current Gemstash::Env.
16
+ module Helper
17
+ private
18
+
19
+ def gemstash_env
20
+ Gemstash::Env.current
21
+ end
22
+ end
23
+
24
+ # Rack middleware to set the Gemstash::Env for the app.
25
+ class RackMiddleware
26
+ def initialize(app, gemstash_env)
27
+ @app = app
28
+ @gemstash_env = gemstash_env
29
+ end
30
+
31
+ def call(env)
32
+ env["gemstash.env"] = @gemstash_env
33
+ @app.call(env)
34
+ end
35
+ end
36
+
37
+ def initialize(config = nil, cache: nil, db: nil)
38
+ @config = config
39
+ @cache = cache
40
+ @db = db
41
+ end
42
+
43
+ def self.available?
44
+ !Thread.current[:gemstash_env].nil?
45
+ end
46
+
47
+ def self.current
48
+ raise EnvNotSetError unless Thread.current[:gemstash_env]
49
+ Thread.current[:gemstash_env]
50
+ end
51
+
52
+ def self.current=(value)
53
+ Thread.current[:gemstash_env] = value
54
+ end
55
+
56
+ def self.daemonized?
57
+ if @daemonized.nil?
58
+ raise "Daemonized hasn't been set yet!"
59
+ else
60
+ @daemonized
61
+ end
62
+ end
63
+
64
+ def self.daemonized=(value)
65
+ value = false if value.nil?
66
+ @daemonized = value
67
+ end
68
+
69
+ def config
70
+ @config ||= Gemstash::Configuration.new
71
+ end
72
+
73
+ def config=(value)
74
+ reset
75
+ @config = value
76
+ end
77
+
78
+ def reset
79
+ @config = nil
80
+ @cache = nil
81
+ @cache_client = nil
82
+ @db = nil
83
+ end
84
+
85
+ def base_path
86
+ dir = config[:base_path]
87
+
88
+ if config.default?(:base_path)
89
+ FileUtils.mkpath(dir) unless Dir.exist?(dir)
90
+ else
91
+ raise "Base path '#{dir}' is not writable" unless File.writable?(dir)
92
+ end
93
+
94
+ dir
95
+ end
96
+
97
+ def base_file(path)
98
+ File.join(base_path, path)
99
+ end
100
+
101
+ def rackup
102
+ File.expand_path("../config.ru", __FILE__)
103
+ end
104
+
105
+ def db
106
+ @db ||= begin
107
+ case config[:db_adapter]
108
+ when "sqlite3"
109
+ db_path = base_file("gemstash.db")
110
+
111
+ if RUBY_PLATFORM == "java"
112
+ db = Sequel.connect("jdbc:sqlite:#{db_path}", max_connections: 1)
113
+ else
114
+ db = Sequel.connect("sqlite://#{URI.escape(db_path)}", max_connections: 1)
115
+ end
116
+ when "postgres"
117
+ db = Sequel.connect(config[:db_url])
118
+ else
119
+ raise "Unsupported DB adapter: '#{config[:db_adapter]}'"
120
+ end
121
+
122
+ Gemstash::Env.migrate(db)
123
+ db
124
+ end
125
+ end
126
+
127
+ def self.migrate(db)
128
+ Sequel.extension :migration
129
+ migrations_dir = File.expand_path("../migrations", __FILE__)
130
+ Sequel::Migrator.run(db, migrations_dir, :use_transactions => true)
131
+ end
132
+
133
+ def cache
134
+ @cache ||= Gemstash::Cache.new(cache_client)
135
+ end
136
+
137
+ def cache_client
138
+ @cache_client ||= begin
139
+ case config[:cache_type]
140
+ when "memory"
141
+ Gemstash::LruReduxClient.new
142
+ when "memcached"
143
+ Dalli::Client.new(config[:memcached_servers])
144
+ else
145
+ raise "Invalid cache client: '#{config[:cache_type]}'"
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end