Simple private gem server with a rack middleware
30th Aug 2011In my job we have developed some ruby gems to reuse in our internal applications and found the need to set up a private gem server so that every developer could install them in an easy way. To configure an own gem server without using the gem server command is really simple: we just need to create a directory with a folder called gems, place our packaged gems on it and run the command gem generate_index on the base directory:
dagi3d:/www/gemserver# tree
.
`-- gems
`-- foo-0.0.1.gem
dagi3d:/www/gemserver# gem generate_index
Loading 1 gems from .
.
Loaded all gems
loaded: 0.003s
Generating Marshal quick index gemspecs for 1 gems
.
Complete
Generated Marshal quick index gemspecs: 0.001s
Generating Marshal master index
Generated Marshal master index: 0.000s
Generating specs index
Generated specs index: 0.000s
Generating latest specs index
Generated latest specs index: 0.000s
Generating prerelease specs index
Generated prerelease specs index: 0.000s
Compressing indicies
Compressed indicies: 0.002s
dagi3d:/www/gemserver# tree
.
|-- Marshal.4.8
|-- Marshal.4.8.Z
|-- gems
| `-- foo-0.0.1.gem
|-- latest_specs.4.8
|-- latest_specs.4.8.gz
|-- prerelease_specs.4.8
|-- prerelease_specs.4.8.gz
|-- quick
| `-- Marshal.4.8
| `-- foo-0.0.1.gemspec.rz
|-- specs.4.8
`-- specs.4.8.gz
Eventually, we have to configure our webserver by setting a virtual host with this directory as the document root:
server {
listen 80;
server_name rubygems.dagi3d.net;
root /www/gemserver;
}
The problem with this approach is that we have to upload the gem on the server manually and regenerate the index every time, so I came up with the idea of writing a simple rack application that could handle the gems upload and generate the index automatically:
gem_server.rbrequire 'fileutils' require 'rubygems/user_interaction' require 'rubygems/indexer' class GemServer def call(env) request = Rack::Request.new(env) case request.path_info when "/" list_gems(request) when "/push" upload_gem(request) else [404, {"Content-Type" => "text/html"}, ["Page not found"]] end end private def gem_indexer Gem::Indexer.new("#{APP_ROOT}/public") end def upload_gem(request) return [500, {"Content-Type" => "text/html"}, ["Invalid method"]] unless request.post? file = request.params['file'] return [400, {"Content-Type" => "text/html"}, ["No file was given"]] if file.nil? gem_format = Gem::Format.from_file_by_path(file[:tempfile]) return [400, {"Content-Type" => "text/html"}, ["Invalid gem"]] if gem_format.nil? dst_dir = "#{APP_ROOT}/public/gems" FileUtils.mkdir(dst_dir) unless File.exists?(dst_dir) FileUtils.mv file[:tempfile], "#{dst_dir}/#{file[:filename]}" gem_indexer.generate_index [200, {"Content-Type" => "text/html"}, ["OK!"]] end def list_gems(request) out = <<-HTML <html> <body> <ul> #{gem_indexer.gem_file_list.map {|gem| '<li>' + File.basename(gem) + '</li>'}.join} </ul> </body> </html> HTML [200, {"Content-Type" => "text/html"}, [out]] end end
APP_ROOT = File.expand_path(".") require File.expand_path("../gem_server", __FILE__) run GemServer.new
server {
listen 80;
server_name rubygems.dagi3d.net;
root /www/gemserver/public;
passenger_enabled on;
}
Now we can send a POST request to the /push path of our server and the gem will be copied to the public/gems directory and the index will be generated automatically:
require 'rest_client' exit if ARGV.size == 0 file = File.new(ARGV[0]) host = ARGV[1] begin response = RestClient.post(host, :file => file) puts response.inspect rescue Exception => e puts e.inspect end
dagi3d:rack-gem-server dagi3d$ ruby client-test.rb foo-0.0.1.gem http://rubygems.dagi3d.net/push
"OK!"
dagi3d:rack-gem-server dagi3d$
Obviously this is a really naive implementation and has some security flaws as there is no user validation and everyone could publish a gem to the server but I think it can work on certain environments where the users access is restricted and under control.
I've uploaded the code on my github account: https://github.com/dagi3d/rack-gem-server
Michael R.
over 1 year ago