In 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.rb
require '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
config.ru
APP_ROOT = File.expand_path(".")
require File.expand_path("../gem_server", __FILE__)
run GemServer.new
nginx.conf
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:

client-test.rb
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