Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 258 lines (222 sloc) 7.18 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
require 'rubygems'
require 'digest/md5'
require 'builder'
require 'sinatra/base'
require 'rubygems/builder'
require 'rubygems/indexer'
require 'rubygems/package'
require 'hostess'
require 'geminabox/version'
require 'rss/atom'
require 'tempfile'

class Geminabox < Sinatra::Base
  enable :static, :methodoverride

  set :public_folder, File.join(File.dirname(__FILE__), *%w[.. public])
  set :data, File.join(File.dirname(__FILE__), *%w[.. data])
  set :build_legacy, false
  set :incremental_updates, true
  set :views, File.join(File.dirname(__FILE__), *%w[.. views])
  set :allow_replace, false
  set :gem_permissions, 0644
  use Hostess

  class << self
    def disallow_replace?
      ! allow_replace
    end

    def fixup_bundler_rubygems!
      return if @post_reset_hook_applied
      Gem.post_reset{ Gem::Specification.all = nil } if defined? Bundler and Gem.respond_to? :post_reset
      @post_reset_hook_applied = true
    end
  end

  autoload :GemVersionCollection, "geminabox/gem_version_collection"
  autoload :GemVersion, "geminabox/gem_version"
  autoload :DiskCache, "geminabox/disk_cache"
  autoload :IncomingGem, "geminabox/incoming_gem"

  before do
    headers 'X-Powered-By' => "geminabox #{GeminaboxVersion}"
  end

  get '/' do
    @gems = load_gems
    @index_gems = index_gems(@gems)
    erb :index
  end

  get '/atom.xml' do
    @gems = load_gems
    erb :atom, :layout => false
  end

  # Return a list of versions of gem 'gem_name' with the dependencies of each version.
  def gem_dependencies(gem_name)
    dependency_cache.marshal_cache(gem_name) do
      load_gems.select {|gem| gem_name == gem.name }.map do |gem|
        spec = spec_for(gem.name, gem.number)
        {
          :name => gem.name,
          :number => gem.number.version,
          :platform => gem.platform,
          :dependencies => spec.dependencies.select {|dep| dep.type == :runtime}.map {|dep| [dep.name, dep.requirement.to_s] }
        }
      end
    end
  end

  get '/api/v1/dependencies' do
    query_gems = params[:gems].split(',')
    deps = query_gems.inject([]){|memo, query_gem| memo + gem_dependencies(query_gem) }
    Marshal.dump(deps)
  end

  get '/upload' do
    erb :upload
  end

  get '/reindex' do
    reindex(:force_rebuild)
    redirect url("/")
  end

  get '/gems/:gemname' do
    gems = Hash[load_gems.by_name]
    @gem = gems[params[:gemname]]
    halt 404 unless @gem
    erb :gem
  end

  delete '/gems/*.gem' do
    File.delete file_path if File.exists? file_path
    reindex(:force_rebuild)
    redirect url("/")
  end

  post '/upload' do
    unless params[:file] && params[:file][:filename] && (tmpfile = params[:file][:tempfile])
      @error = "No file selected"
      halt [400, erb(:upload)]
    end
    handle_incoming_gem(IncomingGem.new(tmpfile))
  end

  post '/api/v1/gems' do
    begin
      handle_incoming_gem(IncomingGem.new(request.body))
    rescue Object => o
      File.open "/tmp/debug.txt", "a" do |io|
        io.puts o, o.backtrace
      end
    end
  end

private

  def handle_incoming_gem(gem)
    prepare_data_folders
    error_response(400, "Cannot process gem") unless gem.valid?
    handle_replacement(gem)
    write_and_index(gem)

    if api_request?
      "Gem #{gem.name} received and indexed."
    else
      redirect url("/")
    end
  end

  def api_request?
    request.accept.first != "text/html"
  end

  def error_response(code, message)
    halt [code, message] if api_request?
    html = <<HTML
<html>
<head><title>Error - #{code}</title></head>
<body>
<h1>Error - #{code}</h1>
<p>#{message}</p>
</body>
</html>
HTML
    halt [code, html]
  end

  def prepare_data_folders
    if File.exists? Geminabox.data
      error_response( 500, "Please ensure #{File.expand_path(Geminabox.data)} is a directory." ) unless File.directory? Geminabox.data
      error_response( 500, "Please ensure #{File.expand_path(Geminabox.data)} is writable by the geminabox web server." ) unless File.writable? Geminabox.data
    else
      begin
        FileUtils.mkdir_p(settings.data)
      rescue Errno::EACCES, Errno::ENOENT, RuntimeError => e
        error_response( 500, "Could not create #{File.expand_path(Geminabox.data)}.\n#{e}\n#{e.message}" )
      end
    end

    FileUtils.mkdir_p(File.join(settings.data, "gems"))
  end

  def handle_replacement(gem)
    if Geminabox.disallow_replace? and File.exist?(gem.dest_filename)
      existing_file_digest = Digest::SHA1.file(gem.dest_filename).hexdigest

      if existing_file_digest != gem.hexdigest
        error_response(409, "Updating an existing gem is not permitted.\nYou should either delete the existing version, or change your version number.")
      else
        error_response(200, "Ignoring upload, you uploaded the same thing previously.")
      end
    end
  end

  def write_and_index(gem)
    tmpfile = gem.gem_data
    atomic_write(gem.dest_filename) do |f|
      while blk = tmpfile.read(65536)
        f << blk
      end
    end
    reindex
  end

  def reindex(force_rebuild = false)
    Geminabox.fixup_bundler_rubygems!
    force_rebuild = true unless settings.incremental_updates
    if force_rebuild
      indexer.generate_index
      dependency_cache.flush
    else
      begin
        require 'geminabox/indexer'
        updated_gemspecs = Geminabox::Indexer.updated_gemspecs(indexer)
        Geminabox::Indexer.patch_rubygems_update_index_pre_1_8_25(indexer)
        indexer.update_index
        updated_gemspecs.each { |gem| dependency_cache.flush_key(gem.name) }
      rescue => e
        puts "#{e.class}:#{e.message}"
        puts e.backtrace.join("\n")
        reindex(:force_rebuild)
      end
    end
  end

  def indexer
    Gem::Indexer.new(settings.data, :build_legacy => settings.build_legacy)
  end

  def file_path
    File.expand_path(File.join(settings.data, *request.path_info))
  end

  def dependency_cache
    @dependency_cache ||= Geminabox::DiskCache.new(File.join(settings.data, "_cache"))
  end

  def all_gems
    %w(specs prerelease_specs).map{ |specs_file_type|
      specs_file_path = File.join(settings.data, "#{specs_file_type}.#{Gem.marshal_version}.gz")
      if File.exists?(specs_file_path)
        Marshal.load(Gem.gunzip(Gem.read_binary(specs_file_path)))
      else
        []
      end
    }.inject(:|)
  end

  def load_gems
    @loaded_gems ||= Geminabox::GemVersionCollection.new(all_gems)
  end

  def index_gems(gems)
    Set.new(gems.map{|gem| gem.name[0..0].downcase})
  end

  # based on http://as.rubyonrails.org/classes/File.html
  def atomic_write(file_name)
    temp_dir = File.join(settings.data, "_temp")
    FileUtils.mkdir_p(temp_dir)
    temp_file = Tempfile.new("." + File.basename(file_name), temp_dir, {:binmode => true})
    yield temp_file
    temp_file.close
    File.rename(temp_file.path, file_name)
    File.chmod(settings.gem_permissions, file_name)
  end

  helpers do
    def spec_for(gem_name, version)
      spec_file = File.join(settings.data, "quick", "Marshal.#{Gem.marshal_version}", "#{gem_name}-#{version}.gemspec.rz")
      File.open(spec_file, 'rb') { |file| Marshal.load(Gem.inflate(file.read)) } if File.exists? spec_file
    end
  end
end
Something went wrong with that request. Please try again.