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