mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
fbf59bdbea
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@13862 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
504 lines
12 KiB
Ruby
504 lines
12 KiB
Ruby
require 'webrick'
|
|
require 'rdoc/template'
|
|
require 'yaml'
|
|
require 'zlib'
|
|
|
|
require 'rubygems'
|
|
|
|
##
|
|
# 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 folowing:
|
|
# * "/" - Browsing of gem spec files for installed gems
|
|
# * "/Marshal" - Full SourceIndex dump of metadata for installed gems
|
|
# * "/yaml" - YAML dump of metadata for installed gems - deprecated
|
|
# * "/gems" - Direct access to download the installable gems
|
|
#
|
|
# == Usage
|
|
#
|
|
# gem server [-p portnum] [-d gem_path]
|
|
#
|
|
# port_num:: The TCP port the HTTP server will bind to
|
|
# gem_path::
|
|
# Root gem directory containing both "cache" and "specifications"
|
|
# subdirectories.
|
|
class Gem::Server
|
|
|
|
include Gem::UserInteraction
|
|
|
|
DOC_TEMPLATE = <<-WEBPAGE
|
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
<!DOCTYPE html
|
|
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
|
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
<head>
|
|
<title>RubyGems Documentation Index</title>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
|
<link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
|
|
</head>
|
|
<body>
|
|
<div id="fileHeader">
|
|
<h1>RubyGems Documentation Index</h1>
|
|
</div>
|
|
<!-- banner header -->
|
|
|
|
<div id="bodyContent">
|
|
<div id="contextContent">
|
|
<div id="description">
|
|
<h1>Summary</h1>
|
|
<p>There are %gem_count% gems installed:</p>
|
|
<p>
|
|
START:specs
|
|
IFNOT:is_last
|
|
<a href="#%name%">%name%</a>,
|
|
ENDIF:is_last
|
|
IF:is_last
|
|
<a href="#%name%">%name%</a>.
|
|
ENDIF:is_last
|
|
END:specs
|
|
<h1>Gems</h1>
|
|
|
|
<dl>
|
|
START:specs
|
|
<dt>
|
|
IF:first_name_entry
|
|
<a name="%name%"></a>
|
|
ENDIF:first_name_entry
|
|
<b>%name% %version%</b>
|
|
IF:rdoc_installed
|
|
<a href="%doc_path%">[rdoc]</a>
|
|
ENDIF:rdoc_installed
|
|
IFNOT:rdoc_installed
|
|
<span title="rdoc not installed">[rdoc]</span>
|
|
ENDIF:rdoc_installed
|
|
IF:homepage
|
|
<a href="%homepage%" title="%homepage%">[www]</a>
|
|
ENDIF:homepage
|
|
IFNOT:homepage
|
|
<span title="no homepage available">[www]</span>
|
|
ENDIF:homepage
|
|
IF:has_deps
|
|
- depends on
|
|
START:dependencies
|
|
IFNOT:is_last
|
|
<a href="#%name%" title="%version%">%name%</a>,
|
|
ENDIF:is_last
|
|
IF:is_last
|
|
<a href="#%name%" title="%version%">%name%</a>.
|
|
ENDIF:is_last
|
|
END:dependencies
|
|
ENDIF:has_deps
|
|
</dt>
|
|
<dd>
|
|
%summary%
|
|
IF:executables
|
|
<br/>
|
|
|
|
IF:only_one_executable
|
|
Executable is
|
|
ENDIF:only_one_executable
|
|
|
|
IFNOT:only_one_executable
|
|
Executables are
|
|
ENDIF:only_one_executable
|
|
|
|
START:executables
|
|
IFNOT:is_last
|
|
<span class="context-item-name">%executable%</span>,
|
|
ENDIF:is_last
|
|
IF:is_last
|
|
<span class="context-item-name">%executable%</span>.
|
|
ENDIF:is_last
|
|
END:executables
|
|
ENDIF:executables
|
|
<br/>
|
|
<br/>
|
|
</dd>
|
|
END:specs
|
|
</dl>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="validator-badges">
|
|
<p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
WEBPAGE
|
|
|
|
# CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108
|
|
RDOC_CSS = <<-RDOCCSS
|
|
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; }
|
|
RDOCCSS
|
|
|
|
def self.run(options)
|
|
new(options[:gemdir], options[:port], options[:daemon]).run
|
|
end
|
|
|
|
def initialize(gemdir, port, daemon)
|
|
Socket.do_not_reverse_lookup = true
|
|
|
|
@gemdir = gemdir
|
|
@port = port
|
|
@daemon = daemon
|
|
logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL
|
|
@server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger
|
|
|
|
@spec_dir = File.join @gemdir, "specifications"
|
|
@source_index = Gem::SourceIndex.from_gems_in @spec_dir
|
|
end
|
|
|
|
def quick(req, res)
|
|
res['content-type'] = 'text/plain'
|
|
res['date'] = File.stat(@spec_dir).mtime
|
|
|
|
case req.request_uri.request_uri
|
|
when '/quick/index' then
|
|
res.body << @source_index.map { |name,_| name }.join("\n")
|
|
when '/quick/index.rz' then
|
|
index = @source_index.map { |name,_| name }.join("\n")
|
|
res.body << Zlib::Deflate.deflate(index)
|
|
when %r|^/quick/(.*)-([0-9.]+)\.gemspec(\.marshal)?\.rz$| then
|
|
specs = @source_index.search $1, $2
|
|
if specs.empty? then
|
|
res.status = 404
|
|
elsif specs.length > 1 then
|
|
res.status = 500
|
|
elsif $3 # marshal quickindex instead of YAML
|
|
res.body << Zlib::Deflate.deflate(Marshal.dump(specs.first))
|
|
else # deprecated YAML format
|
|
res.body << Zlib::Deflate.deflate(specs.first.to_yaml)
|
|
end
|
|
else
|
|
res.status = 404
|
|
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") do |req, res|
|
|
res['content-type'] = 'text/plain'
|
|
res['date'] = File.stat(@spec_dir).mtime
|
|
if req.request_method == 'HEAD' then
|
|
res['content-length'] = @source_index.to_yaml.length
|
|
else
|
|
res.body << @source_index.to_yaml
|
|
end
|
|
end
|
|
|
|
@server.mount_proc("/Marshal") do |req, res|
|
|
res['content-type'] = 'text/plain'
|
|
res['date'] = File.stat(@spec_dir).mtime
|
|
if req.request_method == 'HEAD' then
|
|
res['content-length'] = Marshal.dump(@source_index).length
|
|
else
|
|
res.body << Marshal.dump(@source_index)
|
|
end
|
|
end
|
|
|
|
@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("/") do |req, res|
|
|
specs = []
|
|
total_file_count = 0
|
|
|
|
@source_index.each do |path, spec|
|
|
total_file_count += spec.files.size
|
|
deps = spec.dependencies.collect { |dep|
|
|
{ "name" => dep.name,
|
|
"version" => dep.version_requirements.to_s, }
|
|
}
|
|
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 = TemplatePage.new(DOC_TEMPLATE)
|
|
res['content-type'] = 'text/html'
|
|
template.write_html_on res.body,
|
|
"gem_count" => specs.size.to_s, "specs" => specs,
|
|
"total_file_count" => total_file_count.to_s
|
|
end
|
|
|
|
paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" }
|
|
paths.each do |mount_point, mount_dir|
|
|
@server.mount(mount_point, WEBrick::HTTPServlet::FileHandler,
|
|
File.join(@gemdir, mount_dir), true)
|
|
end
|
|
|
|
trap("INT") { @server.shutdown; exit! }
|
|
trap("TERM") { @server.shutdown; exit! }
|
|
|
|
@server.start
|
|
end
|
|
|
|
end
|
|
|