aboutsummaryrefslogtreecommitdiffstats
path: root/lib/rubygems/server.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems/server.rb')
-rw-r--r--lib/rubygems/server.rb504
1 files changed, 504 insertions, 0 deletions
diff --git a/lib/rubygems/server.rb b/lib/rubygems/server.rb
new file mode 100644
index 0000000000..212ccc5f7e
--- /dev/null
+++ b/lib/rubygems/server.rb
@@ -0,0 +1,504 @@
+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
+