require 'webrick' require 'yaml' require 'zlib' require 'erb' require 'rubygems' require 'rubygems/doc_manager' ## # Gem::Server and allows users to serve gems for consumption by # `gem --remote-install`. # # gem_server starts an HTTP server on the given port and serves the following: # * "/" - Browsing of gem spec files for installed gems # * "/specs.#{Gem.marshal_version}.gz" - specs name/version/platform index # * "/latest_specs.#{Gem.marshal_version}.gz" - latest specs # name/version/platform index # * "/quick/" - Individual gemspecs # * "/gems" - Direct access to download the installable gems # * "/rdoc?q=" - Search for installed rdoc documentation # * legacy indexes: # * "/Marshal.#{Gem.marshal_version}" - Full SourceIndex dump of metadata # for installed gems # * "/yaml" - YAML dump of metadata for installed gems - deprecated # # == Usage # # gem_server = Gem::Server.new Gem.dir, 8089, false # gem_server.run # #-- # TODO Refactor into a real WEBrick servlet to remove code duplication. class Gem::Server include ERB::Util include Gem::UserInteraction SEARCH = <<-SEARCH
SEARCH DOC_TEMPLATE = <<-'DOC_TEMPLATE' RubyGems Documentation Index
<%= SEARCH %>

RubyGems Documentation Index

Summary

There are <%=values["gem_count"]%> gems installed:

<%= values["specs"].map { |v| "#{v["name"]}" }.join ', ' %>.

Gems

<% values["specs"].each do |spec| %>
<% if spec["first_name_entry"] then %> "> <% end %> <%=spec["name"]%> <%=spec["version"]%> <% if spec["rdoc_installed"] then %> ">[rdoc] <% else %> [rdoc] <% end %> <% if spec["homepage"] then %> " title="<%=spec["homepage"]%>">[www] <% else %> [www] <% end %> <% if spec["has_deps"] then %> - depends on <%= spec["dependencies"].map { |v| "#{v["name"]}" }.join ', ' %>. <% end %>
<%=spec["summary"]%> <% if spec["executables"] then %>
<% if spec["only_one_executable"] then %> Executable is <% else %> Executables are <%end%> <%= spec["executables"].map { |v| "#{v["executable"]}"}.join ', ' %>. <%end%>

<% end %>

[Validate]

DOC_TEMPLATE # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108 RDOC_CSS = <<-RDOC_CSS body { font-family: Verdana,Arial,Helvetica,sans-serif; font-size: 90%; margin: 0; margin-left: 40px; padding: 0; background: white; } h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; } h1 { font-size: 150%; } h2,h3,h4 { margin-top: 1em; } a { background: #eef; color: #039; text-decoration: none; } a:hover { background: #039; color: #eef; } /* Override the base stylesheets Anchor inside a table cell */ td > a { background: transparent; color: #039; text-decoration: none; } /* and inside a section title */ .section-title > a { background: transparent; color: #eee; text-decoration: none; } /* === Structural elements =================================== */ div#index { margin: 0; margin-left: -40px; padding: 0; font-size: 90%; } div#index a { margin-left: 0.7em; } div#index .section-bar { margin-left: 0px; padding-left: 0.7em; background: #ccc; font-size: small; } div#classHeader, div#fileHeader { width: auto; color: white; padding: 0.5em 1.5em 0.5em 1.5em; margin: 0; margin-left: -40px; border-bottom: 3px solid #006; } div#classHeader a, div#fileHeader a { background: inherit; color: white; } div#classHeader td, div#fileHeader td { background: inherit; color: white; } div#fileHeader { background: #057; } div#classHeader { background: #048; } .class-name-in-header { font-size: 180%; font-weight: bold; } div#bodyContent { padding: 0 1.5em 0 1.5em; } div#description { padding: 0.5em 1.5em; background: #efefef; border: 1px dotted #999; } div#description h1,h2,h3,h4,h5,h6 { color: #125;; background: transparent; } div#validator-badges { text-align: center; } div#validator-badges img { border: 0; } div#copyright { color: #333; background: #efefef; font: 0.75em sans-serif; margin-top: 5em; margin-bottom: 0; padding: 0.5em 2em; } /* === Classes =================================== */ table.header-table { color: white; font-size: small; } .type-note { font-size: small; color: #DEDEDE; } .xxsection-bar { background: #eee; color: #333; padding: 3px; } .section-bar { color: #333; border-bottom: 1px solid #999; margin-left: -20px; } .section-title { background: #79a; color: #eee; padding: 3px; margin-top: 2em; margin-left: -30px; border: 1px solid #999; } .top-aligned-row { vertical-align: top } .bottom-aligned-row { vertical-align: bottom } /* --- Context section classes ----------------------- */ .context-row { } .context-item-name { font-family: monospace; font-weight: bold; color: black; } .context-item-value { font-size: small; color: #448; } .context-item-desc { color: #333; padding-left: 2em; } /* --- Method classes -------------------------- */ .method-detail { background: #efefef; padding: 0; margin-top: 0.5em; margin-bottom: 1em; border: 1px dotted #ccc; } .method-heading { color: black; background: #ccc; border-bottom: 1px solid #666; padding: 0.2em 0.5em 0 0.5em; } .method-signature { color: black; background: inherit; } .method-name { font-weight: bold; } .method-args { font-style: italic; } .method-description { padding: 0 0.5em 0 0.5em; } /* --- Source code sections -------------------- */ a.source-toggle { font-size: 90%; } div.method-source-code { background: #262626; color: #ffdead; margin: 1em; padding: 0.5em; border: 1px dashed #999; overflow: hidden; } div.method-source-code pre { color: #ffdead; overflow: hidden; } /* --- Ruby keyword styles --------------------- */ .standalone-code { background: #221111; color: #ffdead; overflow: hidden; } .ruby-constant { color: #7fffd4; background: transparent; } .ruby-keyword { color: #00ffff; background: transparent; } .ruby-ivar { color: #eedd82; background: transparent; } .ruby-operator { color: #00ffee; background: transparent; } .ruby-identifier { color: #ffdead; background: transparent; } .ruby-node { color: #ffa07a; background: transparent; } .ruby-comment { color: #b22222; font-weight: bold; background: transparent; } .ruby-regexp { color: #ffa07a; background: transparent; } .ruby-value { color: #7fffd4; background: transparent; } RDOC_CSS RDOC_NO_DOCUMENTATION = <<-'NO_DOC' Found documentation
<%= SEARCH %>

No documentation found

No gems matched <%= h query.inspect %>

Back to complete gem index

[Validate]

NO_DOC RDOC_SEARCH_TEMPLATE = <<-'RDOC_SEARCH' Found documentation
<%= SEARCH %>

Found documentation

Summary

<%=doc_items.length%> documentation topics found.

Topics

<% doc_items.each do |doc_item| %>
<%=doc_item[:name]%> [rdoc]
<%=doc_item[:summary]%>

<% end %>

Back to complete gem index

[Validate]

RDOC_SEARCH def self.run(options) new(options[:gemdir], options[:port], options[:daemon]).run end def initialize(gem_dir, port, daemon) Socket.do_not_reverse_lookup = true @gem_dir = gem_dir @port = port @daemon = daemon logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger @spec_dir = File.join @gem_dir, 'specifications' unless File.directory? @spec_dir then raise ArgumentError, "#{@gem_dir} does not appear to be a gem repository" end @source_index = Gem::SourceIndex.from_gems_in @spec_dir end def Marshal(req, res) @source_index.refresh! res['date'] = File.stat(@spec_dir).mtime index = Marshal.dump @source_index if req.request_method == 'HEAD' then res['content-length'] = index.length return end if req.path =~ /Z$/ then res['content-type'] = 'application/x-deflate' index = Gem.deflate index else res['content-type'] = 'application/octet-stream' end res.body << index end def latest_specs(req, res) @source_index.refresh! res['content-type'] = 'application/x-gzip' res['date'] = File.stat(@spec_dir).mtime specs = @source_index.latest_specs.sort.map do |spec| platform = spec.original_platform platform = Gem::Platform::RUBY if platform.nil? [spec.name, spec.version, platform] end specs = Marshal.dump specs if req.path =~ /\.gz$/ then specs = Gem.gzip specs res['content-type'] = 'application/x-gzip' else res['content-type'] = 'application/octet-stream' end if req.request_method == 'HEAD' then res['content-length'] = specs.length else res.body << specs end end def quick(req, res) @source_index.refresh! res['content-type'] = 'text/plain' res['date'] = File.stat(@spec_dir).mtime case req.request_uri.path when '/quick/index' then res.body << @source_index.map { |name,| name }.sort.join("\n") when '/quick/index.rz' then index = @source_index.map { |name,| name }.sort.join("\n") res['content-type'] = 'application/x-deflate' res.body << Gem.deflate(index) when '/quick/latest_index' then index = @source_index.latest_specs.map { |spec| spec.full_name } res.body << index.sort.join("\n") when '/quick/latest_index.rz' then index = @source_index.latest_specs.map { |spec| spec.full_name } res['content-type'] = 'application/x-deflate' res.body << Gem.deflate(index.sort.join("\n")) when %r|^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)-([0-9.]+)(-.*?)?\.gemspec\.rz$| then dep = Gem::Dependency.new $2, $3 specs = @source_index.search dep marshal_format = $1 selector = [$2, $3, $4].map { |s| s.inspect }.join ' ' platform = if $4 then Gem::Platform.new $4.sub(/^-/, '') else Gem::Platform::RUBY end specs = specs.select { |s| s.platform == platform } if specs.empty? then res.status = 404 res.body = "No gems found matching #{selector}" elsif specs.length > 1 then res.status = 500 res.body = "Multiple gems found matching #{selector}" elsif marshal_format then res['content-type'] = 'application/x-deflate' res.body << Gem.deflate(Marshal.dump(specs.first)) else # deprecated YAML format res['content-type'] = 'application/x-deflate' res.body << Gem.deflate(specs.first.to_yaml) end else raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." end end def root(req, res) @source_index.refresh! res['date'] = File.stat(@spec_dir).mtime raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless req.path == '/' specs = [] total_file_count = 0 @source_index.each do |path, spec| total_file_count += spec.files.size deps = spec.dependencies.map do |dep| { "name" => dep.name, "type" => dep.type, "version" => dep.version_requirements.to_s, } end deps = deps.sort_by { |dep| [dep["name"].downcase, dep["version"]] } deps.last["is_last"] = true unless deps.empty? # executables executables = spec.executables.sort.collect { |exec| {"executable" => exec} } executables = nil if executables.empty? executables.last["is_last"] = true if executables specs << { "authors" => spec.authors.sort.join(", "), "date" => spec.date.to_s, "dependencies" => deps, "doc_path" => "/doc_root/#{spec.full_name}/rdoc/index.html", "executables" => executables, "only_one_executable" => (executables && executables.size == 1), "full_name" => spec.full_name, "has_deps" => !deps.empty?, "homepage" => spec.homepage, "name" => spec.name, "rdoc_installed" => Gem::DocManager.new(spec).rdoc_installed?, "summary" => spec.summary, "version" => spec.version.to_s, } end specs << { "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others", "dependencies" => [], "doc_path" => "/doc_root/rubygems-#{Gem::RubyGemsVersion}/rdoc/index.html", "executables" => [{"executable" => 'gem', "is_last" => true}], "only_one_executable" => true, "full_name" => "rubygems-#{Gem::RubyGemsVersion}", "has_deps" => false, "homepage" => "http://rubygems.org/", "name" => 'rubygems', "rdoc_installed" => true, "summary" => "RubyGems itself", "version" => Gem::RubyGemsVersion, } specs = specs.sort_by { |spec| [spec["name"].downcase, spec["version"]] } specs.last["is_last"] = true # tag all specs with first_name_entry last_spec = nil specs.each do |spec| is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase) spec["first_name_entry"] = is_first last_spec = spec end # create page from template template = ERB.new(DOC_TEMPLATE) res['content-type'] = 'text/html' values = { "gem_count" => specs.size.to_s, "specs" => specs, "total_file_count" => total_file_count.to_s } result = template.result binding res.body = result end ## # Can be used for quick navigation to the rdoc documentation. You can then # define a search shortcut for your browser. E.g. in Firefox connect # 'shortcut:rdoc' to http://localhost:8808/rdoc?q=%s template. Then you can # directly open the ActionPack documentation by typing 'rdoc actionp'. If # there are multiple hits for the search term, they are presented as a list # with links. # # Search algorithm aims for an intuitive search: # 1. first try to find the gems and documentation folders which name # starts with the search term # 2. search for entries, that *contain* the search term # 3. show all the gems # # If there is only one search hit, user is immediately redirected to the # documentation for the particular gem, otherwise a list with results is # shown. # # === Additional trick - install documentation for ruby core # # Note: please adjust paths accordingly use for example 'locate yaml.rb' and # 'gem environment' to identify directories, that are specific for your # local installation # # 1. install ruby sources # cd /usr/src # sudo apt-get source ruby # # 2. generate documentation # rdoc -o /usr/lib/ruby/gems/1.8/doc/core/rdoc \ # /usr/lib/ruby/1.8 ruby1.8-1.8.7.72 # # By typing 'rdoc core' you can now access the core documentation def rdoc(req, res) query = req.query['q'] show_rdoc_for_pattern("#{query}*", res) && return show_rdoc_for_pattern("*#{query}*", res) && return template = ERB.new RDOC_NO_DOCUMENTATION res['content-type'] = 'text/html' res.body = template.result binding end ## # Returns true and prepares http response, if rdoc for the requested gem # name pattern was found. # # The search is based on the file system content, not on the gems metadata. # This allows additional documentation folders like 'core' for the ruby core # documentation - just put it underneath the main doc folder. def show_rdoc_for_pattern(pattern, res) found_gems = Dir.glob("#{@gem_dir}/doc/#{pattern}").select {|path| File.exist? File.join(path, 'rdoc/index.html') } case found_gems.length when 0 return false when 1 new_path = File.basename(found_gems[0]) res.status = 302 res['Location'] = "/doc_root/#{new_path}/rdoc/index.html" return true else doc_items = [] found_gems.each do |file_name| base_name = File.basename(file_name) doc_items << { :name => base_name, :url => "/doc_root/#{base_name}/rdoc/index.html", :summary => '' } end template = ERB.new(RDOC_SEARCH_TEMPLATE) res['content-type'] = 'text/html' result = template.result binding res.body = result return true end end def run @server.listen nil, @port say "Starting gem server on http://localhost:#{@port}/" WEBrick::Daemon.start if @daemon @server.mount_proc "/yaml", method(:yaml) @server.mount_proc "/yaml.Z", method(:yaml) @server.mount_proc "/Marshal.#{Gem.marshal_version}", method(:Marshal) @server.mount_proc "/Marshal.#{Gem.marshal_version}.Z", method(:Marshal) @server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs) @server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs) @server.mount_proc "/latest_specs.#{Gem.marshal_version}", method(:latest_specs) @server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz", method(:latest_specs) @server.mount_proc "/quick/", method(:quick) @server.mount_proc("/gem-server-rdoc-style.css") do |req, res| res['content-type'] = 'text/css' res['date'] = File.stat(@spec_dir).mtime res.body << RDOC_CSS end @server.mount_proc "/", method(:root) @server.mount_proc "/rdoc", method(:rdoc) paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" } paths.each do |mount_point, mount_dir| @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler, File.join(@gem_dir, mount_dir), true) end trap("INT") { @server.shutdown; exit! } trap("TERM") { @server.shutdown; exit! } @server.start end def specs(req, res) @source_index.refresh! res['date'] = File.stat(@spec_dir).mtime specs = @source_index.sort.map do |_, spec| platform = spec.original_platform platform = Gem::Platform::RUBY if platform.nil? [spec.name, spec.version, platform] end specs = Marshal.dump specs if req.path =~ /\.gz$/ then specs = Gem.gzip specs res['content-type'] = 'application/x-gzip' else res['content-type'] = 'application/octet-stream' end if req.request_method == 'HEAD' then res['content-length'] = specs.length else res.body << specs end end def yaml(req, res) @source_index.refresh! res['date'] = File.stat(@spec_dir).mtime index = @source_index.to_yaml if req.path =~ /Z$/ then res['content-type'] = 'application/x-deflate' index = Gem.deflate index else res['content-type'] = 'text/plain' end if req.request_method == 'HEAD' then res['content-length'] = index.length return end res.body << index end end