require "zip/zip"
require "time" # for Time.httpdate
require 'rack/utils'
require 'rack/mime'
require 'rexml/document'
module Rack
class XapBuilder
F = ::File
X = ::REXML
class << self
def xap_to_memory(root)
end
def xap_to_disk(options, xap_file="")
xap_file = options[:xap_name] if xap_file.to_s.empty?
xap_file = "#{xap_file}.xap" if xap_file !~ /\.xap$/
collect_metadata(options) unless options.key?(:application_files)
pth = F.join(options[:root], options[:xap_path], xap_file)
FileUtils.mkdir_p(F.dirname(pth)) unless F.exist?(F.dirname(pth))
Zip::ZipFile.open(pth, Zip::ZipFile::CREATE) do |zipfile|
xap_files(zipfile, options)
end
end
def collect_metadata(options)
options[:application_files] = collect_application_files options
options[:extra_app_files] = collect_extra_app_files options
options[:assembly_files] = Dir.glob(F.join(options[:assembly_path],"**","*"))
options[:languages_in_use] = find_languages_in_use options
end
private
def xap_files(archive, options)
add_dlr_assemblies archive, options
add_languages archive, options
add_extra_directories archive, options
add_application_files archive, options
generate_languages_config archive, options
generate_app_manifest archive, options
end
def generate_app_manifest(archive, options)
archive.get_output_stream("AppManifest.xaml") do |f|
doc = X::Document.new(options[:app_manifest]||Xapper::APP_MANIFEST_TEMPLATE)
options[:added_assemblies].each do |assembly|
ele = X::Element.new "AssemblyPart"
ele.attributes['Source'] = assembly
doc.root.elements['Deployment.Parts'] << ele
end
doc.write f, 1
end
end
def generate_languages_config(archive, options)
archive.get_output_stream("languages.config") do |f|
doc = X::Document.new("")
options[:languages_in_use].each do |language|
ele = X::Element.new "Language"
ele.attributes['names'] = language[:names].join(",")
ele.attributes['extensions'] = language[:extensions].join(",")
ele.attributes['languageContext'] = language[:context]
ele.attributes['assemblies'] = language[:assemblies].join(";")
ele.attributes['external'] = language[:external]
doc.root << ele
end
doc.write f, 1
end
end
def add_dlr_assemblies(archive, options)
add_dlr_assembly archive, options, "Microsoft.Dynamic"
add_dlr_assembly archive, options, "Microsoft.Scripting"
if [:clr2, :clr3, :sl2, :sl3].include?(options[:sl_version].to_sym)
add_dlr_assembly archive, options, "Microsoft.Scripting.Core"
add_dlr_assembly archive, options, "Microsoft.Scripting.ExtensionAttribute"
end
add_dlr_assembly archive, options, "Microsoft.Scripting.Silverlight" if (ENV['RACK_ENV']||'development') != 'production'
end
def add_languages(archive, options)
options[:languages_in_use].each do |language|
language[:assemblies].each do |assembly|
add_dlr_assembly archive, options, assembly
end
end
end
def add_dlr_assembly(archive, options, name)
options[:added_assemblies] ||= []
pth = assembly_path(options, name)
if F.file?(pth)
nm = F.basename pth
archive.add nm, pth
options[:added_assemblies] << nm
end
end
def assembly_path(options, name)
name = "#{name}.dll" unless /\.dll$/ =~ name
F.join(options[:assembly_path], name)
end
def find_languages_in_use(options)
(options[:application_files] + options[:extra_app_files]).inject([]) do |langs, candidate|
lang = options[:languages].find { |lang| lang[:extensions].include?(F.extname(candidate)) }
langs << lang if lang and not langs.include?(lang)
langs
end
end
def collect_application_files(options)
Dir.glob(F.join(options[:source_path], "**", "*"))
end
def collect_extra_app_files(options)
options[:extra_app_files].collect do |pth|
Dir.glob(F.join(pth, "**", "*"))
end.flatten
end
def add_extra_directories(archive, options)
options[:extra_app_files].collect do |pth|
in_ar = F.expand_path(pth).gsub(/^#{FileUtils.pwd}/,'')
archive.add in_ar, pth if F.file?(pth)
end
end
def add_application_files(archive, options)
options[:application_files].each do |path|
in_ar = F.expand_path(path).gsub(/^#{"#{FileUtils.pwd}/#{options[:source_path]}"}\//, '')
archive.add in_ar, path if F.file?(path)
end
end
end
end
class Xapper
Rack::Mime::MIME_TYPES.merge!({
".js" => "application/x-javascript",
".py" => "application/python",
".rb" => "application/ruby",
".zip" => "application/x-zip-compressed",
".slvx" => "application/x-zip-compressed",
".xaml" => "application/xaml+xml",
".xap" => "application/x-zip-compressed"
})
APP_MANIFEST_TEMPLATE = <<-TEMPLEND
TEMPLEND
LANGUAGES=[
{
:names => %w(IronPython Python py),
:extensions => %w(.py),
:context => "IronPython.Runtime.PythonContext",
:assemblies => %w(IronPython.dll IronPython.Modules.dll),
:external => "IronPython.slvx"
},
{
:names => %w(IronRuby Ruby rb),
:extensions => %w(.rb),
:context => "IronRuby.Runtime.RubyContext",
:assemblies => %w(IronRuby.dll IronRuby.Libraries.dll),
:external => "IronRuby.slvx"
}
]
DEFAULT_OPTIONS= {
:assembly_path => ::File.dirname(__FILE__) + '/../assemblies',
:xap_path => "sl",
:source_path => 'app/silverlight',
:extra_app_files => [],
:xap_name => 'app',
:external_url_prefix => "/dlr-slvx",
:root => 'public',
:sl_version => :clr2,
:languages => LANGUAGES,
:app_manifest => APP_MANIFEST_TEMPLATE
}
def initialize(app, options={})
@xap_options = DEFAULT_OPTIONS.merge(options)
@app = app
end
def call(env)
path = env['PATH_INFO']
can_serve = /#{@xap_options[:xap_name]}\.xap/ =~ path
if can_serve
file_server = XapFile.new(@xap_options)
file_server.call(env)
else
@app.call(env)
end
end
end
class XapFile
def initialize(options)
@options = options.dup
xap_name = "#{@options[:xap_name]}"
xap_name = "#{xap_name}.xap" if xap_name !~ /\.xap$/
@xap_path = @options[:root] + "/" + @options[:xap_path] + "/" + xap_name
end
def call(env)
dup._call(env)
end
F = ::File
def _call(env)
@path_info = Utils.unescape(env["PATH_INFO"])
return forbidden if @path_info.include? ".."
if dev_env? or xap_changed?
FileUtils.rm_f(@xap_path) if F.exist?(@xap_path)
XapBuilder.xap_to_disk @options
end
begin
if F.file?(@xap_path) && F.readable?(@xap_path)
[200, {
"Date" => F.mtime(@xap_path).httpdate,
"Content-Type" => Mime.mime_type(F.extname(@xap_path), 'application/x-zip-compressed'),
"Cache-Control" => "no-cache",
"Expires" => "-1",
'Pragma' => "no-cache",
"Content-Length" => F.size?(@xap_path).to_s
}, self]
else
raise Errno::EPERM
end
rescue SystemCallError
not_found
end
end
def dev_env?
(ENV['RACK_ENV'] || 'development').to_sym != :production
end
def xap_changed?
XapBuilder.collect_metadata(@options)
files = @options[:application_files] + @options[:extra_app_files] + @options[:assembly_files]
xap_mtime = File.mtime(@xap_path)
files.any?{ |f| File.mtime(f) > xap_mtime }
end
def forbidden
body = "Forbidden\n"
[403, {"Content-Type" => "text/plain",
"Content-Length" => body.size.to_s},
[body]]
end
def not_found
body = "File not found: #{@path_info}\n"
[404, {"Content-Type" => "text/plain",
"Content-Length" => body.size.to_s},
[body]]
end
def each
F.open(@xap_path, "rb") { |file|
while part = file.read(8192)
yield part
end
}
end
end
end