├── COPYING ├── README ├── Rakefile ├── bin ├── parkplace └── parkseed ├── lib ├── active_record │ └── acts │ │ └── nested_set.rb ├── parkplace.rb └── parkplace │ ├── control.rb │ ├── controllers.rb │ ├── errors.rb │ ├── helpers.rb │ ├── mimetypes_hash.rb │ ├── models.rb │ ├── s3.rb │ └── torrent.rb ├── setup.rb └── static ├── css └── control.css ├── images └── monopoly-car.jpg └── js └── jquery.js /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 why the lucky stiff 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | 3 | Park Place an Amazon-S3 clone 4 | --------------------------------------------------------------------------- 5 | 6 | Okay, so, you've checked out from Subversion and you want to get this suckr 7 | up and everything. 8 | 9 | First, a checklist: 10 | 11 | 1. Do you have Camping and Mongrel installed? 12 | 13 | gem install camping mongrel --include-dependencies 14 | 15 | 2. I would also recommend the `sendfile' gem if you're on non-Windows. 16 | 17 | gem install sendfile 18 | 19 | 3. Do you have SQLite3 installed? (If you don't want to mess with the 20 | database driver.) 21 | 22 | Follow these instructions EXACTLY AS WRITTEN: 23 | http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 24 | 25 | Okay, time to turn it on: 26 | 27 | bin/parkplace 28 | 29 | Once you're sold, you can go ahead and install it for reals: 30 | 31 | sudo ruby setup.rb 32 | parkplace 33 | 34 | --------------------------------------------------------------------------- 35 | for more, visit: http://code.whytheluckystiff.net/parkplace 36 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/clean' 3 | require 'rake/gempackagetask' 4 | require 'rake/rdoctask' 5 | require 'fileutils' 6 | include FileUtils 7 | 8 | NAME = "parkplace" 9 | VERSION = "0.7.22" 10 | CLEAN.include ['**/.*.sw?', '*.gem', '.config'] 11 | 12 | Rake::RDocTask.new do |rdoc| 13 | rdoc.rdoc_dir = 'doc/rdoc' 14 | rdoc.options << '--line-numbers' 15 | rdoc.rdoc_files.add ['README', 'COPYING', 'lib/**/*.rb', 'doc/**/*.rdoc'] 16 | end 17 | 18 | desc "Packages up Park Place." 19 | task :default => [:package] 20 | task :package => [:clean] 21 | 22 | spec = 23 | Gem::Specification.new do |s| 24 | s.name = NAME 25 | s.version = VERSION 26 | s.platform = Gem::Platform::RUBY 27 | s.has_rdoc = true 28 | s.extra_rdoc_files = [ "README" ] 29 | s.summary = "a web file storage service, lovely with BitTorrent support." 30 | s.description = s.summary 31 | s.author = "why the lucky stiff" 32 | s.executables = ['parkplace', 'parkseed'] 33 | 34 | s.add_dependency('mongrel', '>= 0.3.12.5') 35 | s.add_dependency('camping', '>= 1.4.1') 36 | s.add_dependency('sqlite3-ruby', '>=1.1.0') 37 | s.add_dependency('rubytorrent', '>= 0.3') 38 | s.required_ruby_version = '>= 1.8.4' 39 | 40 | s.files = %w(COPYING README Rakefile) + 41 | Dir.glob("{bin,doc,static,test,lib}/**/*") + 42 | Dir.glob("ext/**/*.{h,c,rb}") + 43 | Dir.glob("examples/**/*.rb") + 44 | Dir.glob("tools/*.rb") 45 | 46 | s.require_path = "lib" 47 | # s.extensions = FileList["ext/**/extconf.rb"].to_a 48 | s.bindir = "bin" 49 | end 50 | 51 | Rake::GemPackageTask.new(spec) do |p| 52 | p.need_tar = true 53 | p.gem_spec = spec 54 | end 55 | 56 | task :install do 57 | sh %{rake package} 58 | sh %{sudo gem install pkg/#{NAME}-#{VERSION}} 59 | end 60 | 61 | task :uninstall => [:clean] do 62 | sh %{sudo gem uninstall mongrel} 63 | end 64 | -------------------------------------------------------------------------------- /bin/parkplace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift "./lib" 3 | require 'optparse' 4 | $PARKPLACE_ACCESSORIES = true 5 | require 'rubygems' 6 | gem 'mongrel' 7 | gem 'activesupport', '~> 2.2.0' 8 | gem 'activerecord', '~> 2.2.0' 9 | require 'parkplace' 10 | 11 | DEFAULT_PASSWORD = 'pass@word1' 12 | DEFAULT_SECRET = 'OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV' 13 | 14 | # 15 | 16 | options = ParkPlace.options 17 | options.host = "127.0.0.1" 18 | options.port = 3002 19 | 20 | opts = OptionParser.new do |opts| 21 | opts.banner = "Usage: parkplace [options] [host] [port]" 22 | opts.separator "Default host is #{options.host}, default port is #{options.port}." 23 | 24 | opts.separator "" 25 | opts.separator "Specific options:" 26 | 27 | opts.on("-d", "--directory DIRECTORY", 28 | "Park Place directory (defaults to #{options.parkplace_dir || 'None'})") do |d| 29 | options.parkplace_dir = d 30 | end 31 | 32 | opts.on("-D", "--[no-]daemon", "Daemon mode") do |d| 33 | options.daemon = d 34 | end 35 | 36 | opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| 37 | options.verbose = v 38 | end 39 | 40 | opts.on("-u", "--debug", "Debug Mode") do |v| 41 | begin 42 | require 'ruby-debug' 43 | Debugger.start 44 | rescue LoadError 45 | end 46 | $DEBUG = v 47 | end 48 | 49 | opts.separator "" 50 | opts.separator "Common options:" 51 | 52 | opts.on_tail("-h", "--help", "Show this message") do 53 | puts opts 54 | exit 55 | end 56 | 57 | # Another typical switch to print the version. 58 | opts.on_tail("--version", "Show version") do 59 | puts ParkPlace::VERSION 60 | exit 61 | end 62 | end 63 | 64 | opts.parse! ARGV 65 | options.host = ARGV[0] if ARGV[0] 66 | options.port = ARGV[1].to_i if ARGV[1] 67 | ParkPlace.config(options) 68 | 69 | include ParkPlace 70 | 71 | ParkPlace::Models::Base.establish_connection(options.database) 72 | ParkPlace::Models::Base.logger = Logger.new('camping.log') if $DEBUG 73 | ParkPlace.create 74 | 75 | num_users = ParkPlace::Models::User.count 76 | if num_users == 0 77 | puts "** No users found, creating the `admin' user." 78 | ParkPlace::Models::User.create :login => "admin", :password => DEFAULT_PASSWORD, 79 | :email => "admin@parkplace.net", :key => "44CF9590006BF252F707", :secret => DEFAULT_SECRET, 80 | :activated_at => Time.now, :superuser => 1 81 | end 82 | 83 | admin = ParkPlace::Models::User.find_by_login "admin" 84 | if num_users == 1 and admin and admin.password == hmac_sha1( DEFAULT_PASSWORD, admin.secret ) 85 | puts "** Please login in with `admin' and password `#{DEFAULT_PASSWORD}'" 86 | puts "** You should change the default password for the admin at soonest chance!" 87 | end 88 | 89 | ParkPlace.serve(options.host, options.port) 90 | -------------------------------------------------------------------------------- /bin/parkseed: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift "./lib" 3 | require 'optparse' 4 | require 'parkplace' 5 | require 'stringio' 6 | 7 | options = ParkPlace.options 8 | options.seed_host = "127.0.0.1" 9 | options.seed_port = 3003 10 | 11 | opts = OptionParser.new do |opts| 12 | opts.banner = "Usage: parkseed [options] [host] [port]" 13 | opts.separator "Default host is #{options.seed_host}, default port is #{options.seed_port}." 14 | 15 | opts.separator "" 16 | opts.separator "Specific options:" 17 | 18 | opts.on("-d", "--directory DIRECTORY", 19 | "Park Place directory (defaults to #{options.parkplace_dir || 'None'})") do |d| 20 | options.parkplace_dir = d 21 | end 22 | 23 | opts.on("-D", "--[no-]daemon", "Daemon mode") do |d| 24 | options.daemon = d 25 | end 26 | 27 | opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| 28 | options.verbose = v 29 | end 30 | 31 | opts.separator "" 32 | opts.separator "Common options:" 33 | 34 | opts.on_tail("-h", "--help", "Show this message") do 35 | puts opts 36 | exit 37 | end 38 | 39 | # Another typical switch to print the version. 40 | opts.on_tail("--version", "Show version") do 41 | puts ParkPlace::VERSION 42 | exit 43 | end 44 | end 45 | 46 | opts.parse! ARGV 47 | options.seed_host = ARGV[0] if ARGV[0] 48 | options.seed_port = ARGV[1].to_i if ARGV[1] 49 | ParkPlace.config(options) 50 | 51 | ParkPlace::Models::Base.establish_connection(options.database) 52 | ParkPlace::Models::Base.logger = Logger.new('camping.log') if $DEBUG 53 | ParkPlace.create 54 | 55 | server = RubyTorrent::Server.new(options.seed_host, options.seed_port).start 56 | loop do 57 | ParkPlace::Models::Torrent.find(:all, :include => :bit).each do |trnt| 58 | mi = RubyTorrent::MetaInfo.from_stream(StringIO.new(trnt.metainfo)) 59 | bit = trnt.bit 60 | unless server.instance_variable_get("@controllers").has_key? mi.info.sha1 61 | begin 62 | puts "SEEDING #{bit.name}" 63 | server.add_torrent(mi, RubyTorrent::Package.new(mi, bit.fullpath)) 64 | rescue Exception => e 65 | puts "#{e.class}: #{e.message}" 66 | end 67 | end 68 | end 69 | sleep 2.minutes 70 | end 71 | -------------------------------------------------------------------------------- /lib/active_record/acts/nested_set.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Acts #:nodoc: 3 | module NestedSet #:nodoc: 4 | def self.included(base) 5 | base.extend(ClassMethods) 6 | end 7 | 8 | # This +acts_as+ extension provides Nested Set functionality. Nested Set is similiar to Tree, but with 9 | # the added feature that you can select the children and all of their descendents with 10 | # a single query. A good use case for this is a threaded post system, where you want 11 | # to display every reply to a comment without multiple selects. 12 | # 13 | # A Google search for "Nested Set" should point you to in the right direction to explain the 14 | # database theory. I figured out a bunch of this from 15 | # http://threebit.net/tutorials/nestedset/tutorial1.html 16 | # 17 | # Instead of picturing a leaf node structure with children pointing back to their parent, 18 | # the best way to imagine how this works is to think of the parent entity surrounding all 19 | # of its children, and its parent surrounding it, etc. Assuming that they are lined up 20 | # horizontally, we store the left and right boundries in the database. 21 | # 22 | # Imagine: 23 | # root 24 | # |_ Child 1 25 | # |_ Child 1.1 26 | # |_ Child 1.2 27 | # |_ Child 2 28 | # |_ Child 2.1 29 | # |_ Child 2.2 30 | # 31 | # If my cirlces in circles description didn't make sense, check out this sweet 32 | # ASCII art: 33 | # 34 | # ___________________________________________________________________ 35 | # | Root | 36 | # | ____________________________ ____________________________ | 37 | # | | Child 1 | | Child 2 | | 38 | # | | __________ _________ | | __________ _________ | | 39 | # | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | | 40 | # 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14 41 | # | |___________________________| |___________________________| | 42 | # |___________________________________________________________________| 43 | # 44 | # The numbers represent the left and right boundries. The table then might 45 | # look like this: 46 | # ID | PARENT | LEFT | RIGHT | DATA 47 | # 1 | 0 | 1 | 14 | root 48 | # 2 | 1 | 2 | 7 | Child 1 49 | # 3 | 2 | 3 | 4 | Child 1.1 50 | # 4 | 2 | 5 | 6 | Child 1.2 51 | # 5 | 1 | 8 | 13 | Child 2 52 | # 6 | 5 | 9 | 10 | Child 2.1 53 | # 7 | 5 | 11 | 12 | Child 2.2 54 | # 55 | # So, to get all children of an entry, you 56 | # SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT 57 | # 58 | # To get the count, it's (LEFT - RIGHT + 1)/2, etc. 59 | # 60 | # To get the direct parent, it falls back to using the +PARENT_ID+ field. 61 | # 62 | # There are instance methods for all of these. 63 | # 64 | # The structure is good if you need to group things together; the downside is that 65 | # keeping data integrity is a pain, and both adding and removing an entry 66 | # require a full table write. 67 | # 68 | # This sets up a +before_destroy+ callback to prune the tree correctly if one of its 69 | # elements gets deleted. 70 | # 71 | module ClassMethods 72 | # Configuration options are: 73 | # 74 | # * +parent_column+ - specifies the column name to use for keeping the position integer (default: +parent_id+) 75 | # * +left_column+ - column name for left boundry data, default +lft+ 76 | # * +right_column+ - column name for right boundry data, default +rgt+ 77 | # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach _id 78 | # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible 79 | # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. 80 | # Example: acts_as_nested_set :scope => 'todo_list_id = #{todo_list_id} AND completed = 0' 81 | def acts_as_nested_set(options = {}) 82 | configuration = { :parent_column => "parent_id", :left_column => "lft", :right_column => "rgt", :scope => "1 = 1" } 83 | 84 | configuration.update(options) if options.is_a?(Hash) 85 | 86 | configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/ 87 | 88 | if configuration[:scope].is_a?(Symbol) 89 | scope_condition_method = %( 90 | def scope_condition 91 | if #{configuration[:scope].to_s}.nil? 92 | "#{configuration[:scope].to_s} IS NULL" 93 | else 94 | "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}" 95 | end 96 | end 97 | ) 98 | else 99 | scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end" 100 | end 101 | 102 | class_eval <<-EOV 103 | include ActiveRecord::Acts::NestedSet::InstanceMethods 104 | 105 | #{scope_condition_method} 106 | 107 | def left_col_name() "#{configuration[:left_column]}" end 108 | 109 | def right_col_name() "#{configuration[:right_column]}" end 110 | 111 | def parent_column() "#{configuration[:parent_column]}" end 112 | 113 | EOV 114 | end 115 | end 116 | 117 | module InstanceMethods 118 | # Returns +true+ is this is a root node. 119 | def root? 120 | parent_id = self[parent_column] 121 | (parent_id == 0 || parent_id.nil?) && (self[left_col_name] == 1) && (self[right_col_name] > self[left_col_name]) 122 | end 123 | 124 | # Returns +true+ is this is a child node 125 | def child? 126 | parent_id = self[parent_column] 127 | !(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name]) 128 | end 129 | 130 | # Returns +true+ if we have no idea what this is 131 | def unknown? 132 | !root? && !child? 133 | end 134 | 135 | # Adds a child to this object in the tree. If this object hasn't been initialized, 136 | # it gets set up as a root node. Otherwise, this method will update all of the 137 | # other elements in the tree and shift them to the right, keeping everything 138 | # balanced. 139 | def add_child( child ) 140 | self.reload 141 | child.reload 142 | 143 | if child.root? 144 | raise "Adding sub-tree isn\'t currently supported" 145 | else 146 | if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) ) 147 | # Looks like we're now the root node! Woo 148 | self[left_col_name] = 1 149 | self[right_col_name] = 4 150 | 151 | # What do to do about validation? 152 | return nil unless self.save 153 | 154 | child[parent_column] = self.id 155 | child[left_col_name] = 2 156 | child[right_col_name]= 3 157 | return child.save 158 | else 159 | # OK, we need to add and shift everything else to the right 160 | child[parent_column] = self.id 161 | right_bound = self[right_col_name] 162 | child[left_col_name] = right_bound 163 | child[right_col_name] = right_bound + 1 164 | self[right_col_name] += 2 165 | self.class.base_class.transaction { 166 | self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" ) 167 | self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" ) 168 | self.save 169 | child.save 170 | } 171 | end 172 | end 173 | end 174 | 175 | # Returns the number of nested children of this object. 176 | def children_count 177 | return (self[right_col_name] - self[left_col_name] - 1)/2 178 | end 179 | 180 | # Returns a set of itself and all of its nested children 181 | def full_set 182 | self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" ) 183 | end 184 | 185 | # Returns a set of all of its children and nested children 186 | def all_children 187 | self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" ) 188 | end 189 | 190 | # Returns a set of only this entry's immediate children 191 | def direct_children 192 | self.class.base_class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}", :order => left_col_name) 193 | end 194 | 195 | # Prunes a branch off of the tree, shifting all of the elements on the right 196 | # back to the left so the counts still work. 197 | def before_destroy 198 | return if self[right_col_name].nil? || self[left_col_name].nil? 199 | dif = self[right_col_name] - self[left_col_name] + 1 200 | 201 | self.class.base_class.transaction { 202 | self.class.base_class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" ) 203 | self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})", "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" ) 204 | self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )", "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" ) 205 | } 206 | end 207 | end 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/parkplace.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'camping' 3 | require 'camping/session' 4 | require 'digest/sha1' 5 | require 'base64' 6 | require 'time' 7 | require 'md5' 8 | 9 | require 'active_record/acts/nested_set' 10 | ActiveRecord::Base.send :include, ActiveRecord::Acts::NestedSet 11 | 12 | Camping.goes :ParkPlace 13 | 14 | require 'parkplace/errors' 15 | require 'parkplace/helpers' 16 | require 'parkplace/models' 17 | require 'parkplace/controllers' 18 | if $PARKPLACE_ACCESSORIES 19 | require 'parkplace/control' 20 | end 21 | begin 22 | require 'parkplace/torrent' 23 | puts "-- RubyTorrent found, torrent support is turned on." 24 | puts "-- TORRENT SUPPORT IS EXTREMELY EXPERIMENTAL -- WHAT I MEAN IS: IT PROBABLY DOESN'T WORK." 25 | rescue LoadError 26 | puts "-- No RubyTorrent found, torrent support disbled." 27 | end 28 | require 'parkplace/s3' 29 | 30 | module ParkPlace 31 | VERSION = "0.7" 32 | BUFSIZE = (4 * 1024) 33 | STORAGE_PATH = File.join(Dir.pwd, 'storage') 34 | STATIC_PATH = File.expand_path('../static', File.dirname(__FILE__)) 35 | RESOURCE_TYPES = %w[acl torrent] 36 | CANNED_ACLS = { 37 | 'private' => 0600, 38 | 'public-read' => 0644, 39 | 'public-read-write' => 0666, 40 | 'authenticated-read' => 0640, 41 | 'authenticated-read-write' => 0660 42 | } 43 | READABLE = 0004 44 | WRITABLE = 0002 45 | READABLE_BY_AUTH = 0040 46 | WRITABLE_BY_AUTH = 0020 47 | 48 | class << self 49 | def create 50 | v = 0.0 51 | v = 1.0 if Models::Bucket.table_exists? 52 | Camping::Models::Session.create_schema 53 | Models.create_schema :assume => v 54 | end 55 | def options 56 | require 'ostruct' 57 | options = OpenStruct.new 58 | if options.parkplace_dir.nil? 59 | homes = [] 60 | homes << [ENV['HOME'], File.join( ENV['HOME'], '.parkplace' )] if ENV['HOME'] 61 | homes << [ENV['APPDATA'], File.join( ENV['APPDATA'], 'ParkPlace' )] if ENV['APPDATA'] 62 | homes.each do |home_top, home_dir| 63 | next unless home_top 64 | if File.exists? home_top 65 | options.parkplace_dir = home_dir 66 | break 67 | end 68 | end 69 | end 70 | options 71 | end 72 | def config(options) 73 | require 'ftools' 74 | require 'yaml' 75 | abort "** No home directory found, please say the directory when you run #$O." unless options.parkplace_dir 76 | File.makedirs( options.parkplace_dir ) 77 | conf = File.join( options.parkplace_dir, 'config.yaml' ) 78 | if File.exists? conf 79 | YAML.load_file( conf ).each { |k,v| options.__send__("#{k}=", v) if options.__send__(k).nil? } 80 | end 81 | options.storage_dir = File.expand_path(options.storage_dir || 'storage', options.parkplace_dir) 82 | options.database ||= {:adapter => 'sqlite3', :database => File.join(options.parkplace_dir, 'park.db')} 83 | if options.database[:adapter] == 'sqlite3' 84 | begin 85 | require 'sqlite3_api' 86 | rescue LoadError 87 | puts "!! Your SQLite3 adapter isn't a compiled extension." 88 | abort "!! Please check out http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for tips." 89 | end 90 | end 91 | ParkPlace::STORAGE_PATH.replace options.storage_dir 92 | end 93 | def serve(host, port) 94 | require 'mongrel' 95 | require 'mongrel/camping' 96 | if $PARKPLACE_PROGRESS 97 | require_gem 'mongrel_upload_progress' 98 | GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE 99 | end 100 | 101 | config = Mongrel::Configurator.new :host => host do 102 | listener :port => port do 103 | uri "/", :handler => Mongrel::Camping::CampingHandler.new(ParkPlace) 104 | if $PARKPLACE_PROGRESS 105 | uri "/control/buckets", :handler => plugin('/handlers/upload') 106 | end 107 | uri "/favicon", :handler => Mongrel::Error404Handler.new("") 108 | trap("INT") { stop } 109 | run 110 | end 111 | end 112 | 113 | puts "** ParkPlace example is running at http://#{host}:#{port}/" 114 | puts "** Visit http://#{host}:#{port}/control/ for the control center." 115 | config.join 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/parkplace/control.rb: -------------------------------------------------------------------------------- 1 | require 'parkplace/mimetypes_hash' 2 | 3 | class Class 4 | def login_required 5 | include Camping::Session, ParkPlace::UserSession, ParkPlace::Base 6 | end 7 | end 8 | 9 | module ParkPlace::UserSession 10 | def service(*a) 11 | if @state.user_id 12 | @user = ParkPlace::Models::User.find @state.user_id 13 | end 14 | @state.errors, @state.next_errors = @state.next_errors || [], nil 15 | if @user 16 | super(*a) 17 | else 18 | redirect Controllers::CLogin 19 | end 20 | self 21 | end 22 | end 23 | 24 | module ParkPlace::Controllers 25 | 26 | class CHome < R '/control' 27 | login_required 28 | def get 29 | redirect CBuckets 30 | end 31 | end 32 | 33 | class CLogin < R '/control/login' 34 | include Camping::Session, ParkPlace::Base 35 | def get 36 | render :control, "Login", :login 37 | end 38 | def post 39 | @login = true 40 | @user = User.find_by_login @input.login 41 | if @user 42 | if @user.password == hmac_sha1( @input.password, @user.secret ) 43 | @state.user_id = @user.id 44 | return redirect(CBuckets) 45 | else 46 | @user.errors.add(:password, 'is incorrect') 47 | end 48 | else 49 | @user = User.new 50 | @user.errors.add(:login, 'not found') 51 | end 52 | render :control, "Login", :login 53 | end 54 | end 55 | 56 | class CLogout < R '/control/logout' 57 | login_required 58 | def get 59 | @state.clear 60 | redirect CHome 61 | end 62 | end 63 | 64 | class CBuckets < R '/control/buckets' 65 | login_required 66 | def load_buckets 67 | @buckets = Bucket.find_by_sql [%{ 68 | SELECT b.*, COUNT(c.id) AS total_children 69 | FROM parkplace_bits b LEFT JOIN parkplace_bits c 70 | ON c.parent_id = b.id 71 | WHERE b.parent_id IS NULL AND b.owner_id = ? 72 | GROUP BY b.id ORDER BY b.name}, @user.id] 73 | @bucket = Bucket.new(:owner_id => @user.id, :access => CANNED_ACLS['private']) 74 | end 75 | def get 76 | load_buckets 77 | render :control, 'Your Buckets', :buckets 78 | end 79 | def post 80 | Bucket.find_root(@input.bucket.name) 81 | load_buckets 82 | @bucket.errors.add_to_base("A bucket named `#{@input.bucket.name}' already exists.") 83 | render :control, 'Your Buckets', :buckets 84 | rescue NoSuchBucket 85 | bucket = Bucket.create(@input.bucket) 86 | redirect CBuckets 87 | end 88 | end 89 | 90 | class CFiles < R '/control/buckets/([^\/]+)' 91 | login_required 92 | def get(bucket_name) 93 | @bucket = Bucket.find_root(bucket_name) 94 | only_can_read @bucket 95 | @files = Slot.find :all, :include => :torrent, 96 | :conditions => ['parent_id = ?', @bucket.id], :order => 'name' 97 | render :control, "/#{@bucket.name}", :files 98 | end 99 | def post(bucket_name) 100 | bucket = Bucket.find_root(bucket_name) 101 | only_can_write bucket 102 | 103 | tmpf = @input.upfile.tempfile 104 | readlen, md5 = 0, MD5.new 105 | while part = tmpf.read(BUFSIZE) 106 | readlen += part.size 107 | md5 << part 108 | end 109 | fileinfo = FileInfo.new 110 | fileinfo.mime_type = @input.upfile['type'] || "binary/octet-stream" 111 | fileinfo.size = readlen 112 | fileinfo.md5 = md5.hexdigest 113 | 114 | fileinfo.path = File.join(bucket_name, File.basename(tmpf.path)) 115 | fileinfo.path.succ! while File.exists?(File.join(STORAGE_PATH, fileinfo.path)) 116 | file_path = File.join(STORAGE_PATH, fileinfo.path) 117 | FileUtils.mkdir_p(File.dirname(file_path)) 118 | FileUtils.mv(tmpf.path, file_path) 119 | 120 | @input.fname = @input.upfile.filename if @input.fname.blank? 121 | slot = Slot.create(:name => @input.fname, :owner_id => @user.id, :meta => nil, :obj => fileinfo) 122 | slot.grant(:access => @input.facl.to_i) 123 | bucket.add_child(slot) 124 | redirect CFiles, bucket_name 125 | end 126 | end 127 | 128 | class CFile < R '/control/buckets/([^\/]+?)/(.+)' 129 | login_required 130 | include ParkPlace::SlotGet 131 | end 132 | 133 | class CDeleteBucket < R '/control/delete/([^\/]+)' 134 | login_required 135 | def post(bucket_name) 136 | bucket = Bucket.find_root(bucket_name) 137 | only_owner_of bucket 138 | 139 | if Slot.count(:conditions => ['parent_id = ?', bucket.id]) > 0 140 | error "Bucket #{bucket.name} cannot be deleted, since it is not empty." 141 | else 142 | bucket.destroy 143 | end 144 | redirect CBuckets 145 | end 146 | end 147 | 148 | class CDeleteFile < R '/control/delete/(.+?)/(.+)' 149 | login_required 150 | def post(bucket_name, oid) 151 | bucket = Bucket.find_root bucket_name 152 | only_can_write bucket 153 | slot = bucket.find_slot(oid) 154 | slot.destroy 155 | redirect CFiles, bucket_name 156 | end 157 | end 158 | 159 | class CUsers < R '/control/users' 160 | login_required 161 | def get 162 | only_superusers 163 | @usero = User.new 164 | @users = User.find :all, :conditions => ['deleted != 1'], :order => 'login' 165 | render :control, "User List", :users 166 | end 167 | def post 168 | only_superusers 169 | @usero = User.new @input.user.merge(:activated_at => Time.now) 170 | if @usero.valid? 171 | @usero.save 172 | redirect CUsers 173 | else 174 | render :control, "New User", :user 175 | end 176 | end 177 | end 178 | 179 | class CDeleteUser < R '/control/users/delete/(.+)' 180 | login_required 181 | def post(login) 182 | only_superusers 183 | @usero = User.find_by_login login 184 | if @usero.id == @user.id 185 | error "Suicide is not an option." 186 | else 187 | @usero.destroy 188 | end 189 | redirect CUsers 190 | end 191 | end 192 | 193 | class CUser < R '/control/users/([^\/]+)' 194 | login_required 195 | def get(login) 196 | only_superusers 197 | @usero = User.find_by_login login 198 | render :control, "#{@usero.login}", :profile 199 | end 200 | def post(login) 201 | only_superusers 202 | @usero = User.find_by_login login 203 | @usero.update_attributes(@input.user) 204 | render :control, "#{@usero.login}", :profile 205 | end 206 | end 207 | 208 | class CProgressIndex < R '/control/progress' 209 | def get 210 | Mongrel::Uploads.instance.instance_variable_get("@counters").inspect 211 | end 212 | end 213 | 214 | class CProgress < R '/control/progress/(.+)' 215 | def get(upid) 216 | Mongrel::Uploads.instance.check(upid).inspect 217 | end 218 | end 219 | 220 | class CProfile < R '/control/profile' 221 | login_required 222 | def get 223 | @usero = @user 224 | render :control, "Your Profile", :profile 225 | end 226 | def post 227 | @user.update_attributes(@input.user) 228 | @usero = @user 229 | render :control, "Your Profile", :profile 230 | end 231 | end 232 | 233 | class CStatic < R '/control/s/(.+)' 234 | def get(path) 235 | @headers['Content-Type'] = MIME_TYPES[path[/\.\w+$/, 0]] || "text/plain" 236 | @headers['X-Sendfile'] = File.join(ParkPlace::STATIC_PATH, path) 237 | end 238 | end 239 | end 240 | 241 | module ParkPlace::Views 242 | def control_tab(klass) 243 | opts = {:href => R(klass)} 244 | opts[:class] = (@env.PATH_INFO =~ /^#{opts[:href]}/ ? "active" : "inactive") 245 | opts 246 | end 247 | def control(str, view) 248 | html do 249 | head do 250 | title { "Park Place Control Center » " + str } 251 | script :language => 'javascript', :src => R(CStatic, 'js/jquery.js') 252 | # script :language => 'javascript', :src => R(CStatic, 'js/support.js') 253 | style "@import '#{self / R(CStatic, 'css/control.css')}';", :type => 'text/css' 254 | end 255 | body do 256 | div.page! do 257 | if @user and not @login 258 | div.menu do 259 | ul do 260 | li { a 'buckets', control_tab(CBuckets) } 261 | li { a 'users', control_tab(CUsers) } if @user.superuser? 262 | li { a 'profile', control_tab(CProfile) } 263 | li { a 'logout', control_tab(CLogout) } 264 | end 265 | end 266 | end 267 | div.header! do 268 | h1 "Park Place" 269 | h2 str 270 | end 271 | div.content! do 272 | __send__ "control_#{view}" 273 | end 274 | end 275 | end 276 | end 277 | end 278 | 279 | def control_login 280 | control_loginform 281 | end 282 | 283 | def control_loginform 284 | form :method => 'post', :class => 'create' do 285 | errors_for @user if @user 286 | div.required do 287 | label 'User', :for => 'login' 288 | input.login! :type => 'text' 289 | end 290 | div.required do 291 | label 'Password', :for => 'password' 292 | input.password! :type => 'password' 293 | end 294 | input.loggo! :type => 'submit', :value => "Login" 295 | end 296 | end 297 | 298 | def control_buckets 299 | if @buckets.any? 300 | table do 301 | thead do 302 | th "Name" 303 | th "Contains" 304 | th "Updated on" 305 | th "Permission" 306 | th "Actions" 307 | end 308 | tbody do 309 | @buckets.each do |bucket| 310 | tr do 311 | th { a bucket.name, :href => R(CFiles, bucket.name) } 312 | td "#{bucket.total_children rescue 0} files" 313 | td bucket.updated_at 314 | td bucket.access_readable 315 | td { a "Delete", :href => R(CDeleteBucket, bucket.name), :onClick => POST, :title => "Delete bucket #{bucket.name}" } 316 | end 317 | end 318 | end 319 | end 320 | else 321 | p "A sad day. You have no buckets yet." 322 | end 323 | h3 "Create a Bucket" 324 | form :method => 'post', :class => 'create' do 325 | errors_for @bucket 326 | input :name => 'bucket[owner_id]', :type => 'hidden', :value => @bucket.owner_id 327 | div.required do 328 | label 'Bucket Name', :for => 'bucket[name]' 329 | input :name => 'bucket[name]', :type => 'text', :value => @bucket.name 330 | end 331 | div.required do 332 | label 'Permissions', :for => 'bucket[access]' 333 | select :name => 'bucket[access]' do 334 | ParkPlace::CANNED_ACLS.sort.each do |acl, perm| 335 | opts = {:value => perm} 336 | opts[:selected] = true if perm == @bucket.access 337 | option acl, opts 338 | end 339 | end 340 | end 341 | input.newbucket! :type => 'submit', :value => "Create" 342 | end 343 | end 344 | 345 | def control_files 346 | p "Click on a file name to get file and torrent details." 347 | table do 348 | caption { a(:href => R(CBuckets)) { self << "← Buckets" } } 349 | thead do 350 | th "File" 351 | th "Size" 352 | th "Permission" 353 | end 354 | tbody do 355 | @files.each do |file| 356 | tr do 357 | th do 358 | a file.name, :href => "javascript://", :onclick => "$('#details-#{file.id}').toggle()" 359 | div.details :id => "details-#{file.id}" do 360 | p "Last modified on #{file.updated_at}" 361 | p do 362 | info = [a("Torrent", :href => R(RSlot, @bucket.name, file.name) + "?torrent")] 363 | if file.torrent 364 | info += ["#{file.torrent.seeders} seeders", 365 | "#{file.torrent.leechers} leechers", 366 | "#{file.torrent.total} downloads"] 367 | end 368 | info += [a("Delete", :href => R(CDeleteFile, @bucket.name, file.name), 369 | :onClick => POST, :title => "Delete file #{file.name}")] 370 | info.join " • " 371 | end 372 | end 373 | end 374 | td number_to_human_size(file.obj.size) 375 | td file.access_readable 376 | end 377 | end 378 | end 379 | end 380 | h3 "Upload a File" 381 | form :action => "?upload_id=#{Time.now.to_f}", :method => 'post', :enctype => 'multipart/form-data', :class => 'create' do 382 | div.required do 383 | input :name => 'upfile', :type => 'file' 384 | end 385 | div.optional do 386 | label 'File Name', :for => 'fname' 387 | input :name => 'fname', :type => 'text' 388 | end 389 | div.required do 390 | label 'Permissions', :for => 'facl' 391 | select :name => 'facl' do 392 | ParkPlace::CANNED_ACLS.sort.each do |acl, perm| 393 | opts = {:value => perm} 394 | opts[:selected] = true if perm == @bucket.access 395 | option acl, opts 396 | end 397 | end 398 | end 399 | input.newfile! :type => 'submit', :value => "Create" 400 | end 401 | end 402 | 403 | def control_user 404 | control_userform 405 | end 406 | 407 | def control_userform 408 | form :action => R(CUsers), :method => 'post', :class => 'create' do 409 | errors_for @usero 410 | div.required do 411 | label 'Login', :for => 'user[login]' 412 | input.large :name => 'user[login]', :type => 'text', :value => @usero.login 413 | end 414 | div.required.inline do 415 | label 'Is a super-admin? ', :for => 'user[superuser]' 416 | checkbox 'user[superuser]', @usero.superuser 417 | end 418 | div.required do 419 | label 'Password', :for => 'user[password]' 420 | input.fixed :name => 'user[password]', :type => 'password' 421 | end 422 | div.required do 423 | label 'Password again', :for => 'user[password_confirmation]' 424 | input.fixed :name => 'user[password_confirmation]', :type => 'password' 425 | end 426 | div.required do 427 | label 'Email', :for => 'user[email]' 428 | input :name => 'user[email]', :type => 'text', :value => @usero.email 429 | end 430 | div.required do 431 | label 'Key (must be unique)', :for => 'user[key]' 432 | input.fixed.long :name => 'user[key]', :type => 'text', :value => @usero.key || generate_key 433 | end 434 | div.required do 435 | label 'Secret', :for => 'user[secret]' 436 | input.fixed.long :name => 'user[secret]', :type => 'text', :value => @usero.secret || generate_secret 437 | end 438 | input.newuser! :type => 'submit', :value => "Create" 439 | end 440 | end 441 | 442 | def control_users 443 | errors_for @state 444 | table do 445 | thead do 446 | th "Login" 447 | th "Activated on" 448 | th "Actions" 449 | end 450 | tbody do 451 | @users.each do |user| 452 | tr do 453 | th { a user.login, :href => R(CUser, user.login) } 454 | td user.activated_at 455 | td { a "Delete", :href => R(CDeleteUser, user.login), :onClick => POST, :title => "Delete user #{user.login}" } 456 | end 457 | end 458 | end 459 | end 460 | h3 "Create a User" 461 | control_userform 462 | end 463 | 464 | def control_profile 465 | form :method => 'post', :class => 'create' do 466 | errors_for @usero 467 | if @user.superuser? 468 | div.required.inline do 469 | label 'Is a super-admin? ', :for => 'user[superuser]' 470 | checkbox 'user[superuser]', @usero.superuser 471 | end 472 | end 473 | div.required do 474 | label 'Password', :for => 'user[password]' 475 | input.fixed :name => 'user[password]', :type => 'password' 476 | end 477 | div.required do 478 | label 'Password again', :for => 'user[password_confirmation]' 479 | input.fixed :name => 'user[password_confirmation]', :type => 'password' 480 | end 481 | div.required do 482 | label 'Email', :for => 'user[email]' 483 | input :name => 'user[email]', :type => 'text', :value => @usero.email 484 | end 485 | div.required do 486 | label 'Key', :for => 'key' 487 | h4 @usero.key 488 | end 489 | div.required do 490 | label 'Secret', :for => 'secret' 491 | h4 @usero.secret 492 | end 493 | input.newfile! :type => 'submit', :value => "Save" 494 | # input.regen! :type => 'submit', :value => "Generate New Keys" 495 | end 496 | end 497 | 498 | def number_to_human_size(size) 499 | case 500 | when size < 1.kilobyte: '%d Bytes' % size 501 | when size < 1.megabyte: '%.1f KB' % (size / 1.0.kilobyte) 502 | when size < 1.gigabyte: '%.1f MB' % (size / 1.0.megabyte) 503 | when size < 1.terabyte: '%.1f GB' % (size / 1.0.gigabyte) 504 | else '%.1f TB' % (size / 1.0.terabyte) 505 | end.sub('.0', '') 506 | rescue 507 | nil 508 | end 509 | 510 | def checkbox(name, value) 511 | opts = {:name => name, :type => 'checkbox', :value => 1} 512 | opts[:checked] = "true" if value.to_i == 1 513 | input opts 514 | end 515 | 516 | end 517 | -------------------------------------------------------------------------------- /lib/parkplace/controllers.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module ParkPlace 4 | module SlotGet 5 | def head(bucket_name, oid) 6 | @slot = ParkPlace::Models::Bucket.find_root(bucket_name).find_slot(oid) 7 | only_can_read @slot 8 | 9 | etag = @slot.etag 10 | since = Time.httpdate(@env.HTTP_IF_MODIFIED_SINCE) rescue nil 11 | raise NotModified if since and @slot.updated_at <= since 12 | since = Time.httpdate(@env.HTTP_IF_UNMODIFIED_SINCE) rescue nil 13 | raise PreconditionFailed if since and @slot.updated_at > since 14 | raise PreconditionFailed if @env.HTTP_IF_MATCH and etag != @env.HTTP_IF_MATCH 15 | raise NotModified if @env.HTTP_IF_NONE_MATCH and etag == @env.HTTP_IF_NONE_MATCH 16 | 17 | headers = {} 18 | if @slot.meta 19 | @slot.meta.each { |k, v| headers["x-amz-meta-#{k}"] = v } 20 | end 21 | if @slot.obj.is_a? ParkPlace::Models::FileInfo 22 | headers['Content-Type'] = @slot.obj.mime_type 23 | headers['Content-Disposition'] = @slot.obj.disposition 24 | end 25 | headers['Content-Type'] ||= 'binary/octet-stream' 26 | r(200, '', headers.merge('ETag' => etag, 'Last-Modified' => @slot.updated_at.httpdate, 'Content-Length' => @slot.obj.size)) 27 | end 28 | def get(bucket_name, oid) 29 | head(bucket_name, oid) 30 | if @input.has_key? 'torrent' 31 | torrent @slot 32 | elsif @slot.obj.kind_of?(ParkPlace::Models::FileInfo) && @env.HTTP_RANGE =~ /^bytes=(\d+)?-(\d+)?$/ # yay, parse basic ranges 33 | range_start = $1 34 | range_end = $2 35 | raise NotImplemented unless range_start || range_end # Need at least one or the other. 36 | file_path = File.join(STORAGE_PATH, @slot.obj.path) 37 | file_size = File.size(file_path) 38 | f = File.open(file_path) 39 | if range_start # "Bytes N through ?" mode 40 | range_end = (file_size - 1) if range_end.nil? 41 | content_length = (range_end.to_i - range_start.to_i + 1) 42 | headers['Content-Range'] = "bytes #{range_start.to_i}-#{range_end.to_i}/#{file_size}" 43 | else # "Last N bytes of file" mode. 44 | range_start = file_size - range_end.to_i 45 | content_length = range_end.to_i 46 | headers['Content-Range'] = "bytes #{range_start.to_i}-#{file_size - 1}/#{file_size}" 47 | end 48 | f.seek(range_start.to_i) 49 | @status = 206 50 | headers['Content-Length'] = ([content_length,0].max).to_s 51 | return f 52 | elsif @env.HTTP_RANGE # ugh, parse ranges 53 | raise NotImplemented 54 | else 55 | case @slot.obj 56 | when ParkPlace::Models::FileInfo 57 | file_path = File.join(STORAGE_PATH, @slot.obj.path) 58 | headers['X-Sendfile'] = file_path 59 | else 60 | @slot.obj 61 | end 62 | end 63 | end 64 | end 65 | 66 | module Controllers 67 | def self.S3 *routes 68 | R(*routes).send :include, ParkPlace::S3, Base 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/parkplace/errors.rb: -------------------------------------------------------------------------------- 1 | # == parkplace/errors.rb 2 | # 3 | # Amazon's own S3 service contains a big list of its error codes 4 | # at http://docs.amazonwebservices.com/AmazonS3/2006-03-01/ErrorCodeList.html. 5 | # Park Place builds exception classes for each of these error codes. 6 | # Then, when one of these exceptions is thrown, it is displayed as XML 7 | # in the ParkPlace#service method. 8 | # 9 | # $Author$ 10 | # $Date$ 11 | # 12 | module ParkPlace 13 | 14 | # All errors are derived from ServiceError. It's never actually raised itself, though. 15 | class ServiceError < Exception; end 16 | 17 | # A factory for building exception classes. 18 | YAML::load(<<-END). 19 | AccessDenied: [403, Access Denied] 20 | AllAccessDisabled: [401, All access to this object has been disabled.] 21 | AmbiguousGrantByEmailAddress: [400, The e-mail address you provided is associated with more than one account.] 22 | BadAuthentication: [401, The authorization information you provided is invalid. Please try again.] 23 | BadDigest: [400, The Content-MD5 you specified did not match what we received.] 24 | BucketAlreadyExists: [409, The named bucket you tried to create already exists.] 25 | BucketNotEmpty: [409, The bucket you tried to delete is not empty.] 26 | CredentialsNotSupported: [400, This request does not support credentials.] 27 | EntityTooLarge: [400, Your proposed upload exceeds the maximum allowed object size.] 28 | IncompleteBody: [400, You did not provide the number of bytes specified by the Content-Length HTTP header.] 29 | InternalError: [500, We encountered an internal error. Please try again.] 30 | InvalidArgument: [400, Invalid Argument] 31 | InvalidBucketName: [400, The specified bucket is not valid.] 32 | InvalidDigest: [400, The Content-MD5 you specified was an invalid.] 33 | InvalidRange: [416, The requested range is not satisfiable.] 34 | InvalidSecurity: [403, The provided security credentials are not valid.] 35 | InvalidSOAPRequest: [400, The SOAP request body is invalid.] 36 | InvalidStorageClass: [400, The storage class you specified is not valid.] 37 | InvalidURI: [400, Couldn't parse the specified URI.] 38 | MalformedACLError: [400, The XML you provided was not well-formed or did not validate against our published schema.] 39 | MethodNotAllowed: [405, The specified method is not allowed against this resource.] 40 | MissingContentLength: [411, You must provide the Content-Length HTTP header.] 41 | MissingSecurityElement: [400, The SOAP 1.1 request is missing a security element.] 42 | MissingSecurityHeader: [400, Your request was missing a required header.] 43 | NoSuchBucket: [404, The specified bucket does not exist.] 44 | NoSuchKey: [404, The specified key does not exist.] 45 | NotImplemented: [501, A header you provided implies functionality that is not implemented.] 46 | NotModified: [304, The request resource has not been modified.] 47 | PreconditionFailed: [412, At least one of the pre-conditions you specified did not hold.] 48 | RequestTimeout: [400, Your socket connection to the server was not read from or written to within the timeout period.] 49 | RequestTorrentOfBucketError: [400, Requesting the torrent file of a bucket is not permitted.] 50 | TooManyBuckets: [400, You have attempted to create more buckets than allowed.] 51 | UnexpectedContent: [400, This request does not support content.] 52 | UnresolvableGrantByEmailAddress: [400, The e-mail address you provided does not match any account on record.] 53 | END 54 | each do |code, (status, msg)| 55 | const_set(code, Class.new(ServiceError) { 56 | {:code=>code, :status=>status, :message=>msg}.each do |k,v| 57 | define_method(k) { v } 58 | end 59 | }) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/parkplace/helpers.rb: -------------------------------------------------------------------------------- 1 | class Time 2 | def to_default_s 3 | strftime("%B %d, %Y at %H:%M") 4 | end 5 | end 6 | 7 | module ParkPlace 8 | # For controllers which pass back XML directly, this method allows quick assignment 9 | # of the status code and takes care of generating the XML headers. Takes a block 10 | # which receives the Builder::XmlMarkup object. 11 | def xml status = 200 12 | xml = Builder::XmlMarkup.new 13 | xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8" 14 | yield xml 15 | r(status, xml.target!, 'Content-Type' => 'application/xml') 16 | end 17 | 18 | # Convenient method for generating a SHA1 digest. 19 | def hmac_sha1(key, s) 20 | ipad = [].fill(0x36, 0, 64) 21 | opad = [].fill(0x5C, 0, 64) 22 | key = key.unpack("C*") 23 | if key.length < 64 then 24 | key += [].fill(0, 0, 64-key.length) 25 | end 26 | 27 | inner = [] 28 | 64.times { |i| inner.push(key[i] ^ ipad[i]) } 29 | inner += s.unpack("C*") 30 | 31 | outer = [] 32 | 64.times { |i| outer.push(key[i] ^ opad[i]) } 33 | outer = outer.pack("c*") 34 | outer += Digest::SHA1.digest(inner.pack("c*")) 35 | 36 | return Base64::encode64(Digest::SHA1.digest(outer)).chomp 37 | end 38 | 39 | def generate_secret 40 | abc = %{ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz} 41 | (1..40).map { abc[rand(abc.size),1] }.join 42 | end 43 | 44 | def generate_key 45 | abc = %{ABCDEF0123456789} 46 | (1..20).map { abc[rand(abc.size),1] }.join 47 | end 48 | 49 | POST = %{if(!this.title||confirm(this.title+'?')){var f = document.createElement('form'); this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href; f.submit();}return false;} 50 | 51 | # Kick out anonymous users. 52 | def only_authorized; raise ParkPlace::AccessDenied unless @user end 53 | # Kick out any users which do not have read access to a certain resource. 54 | def only_can_read bit; raise ParkPlace::AccessDenied unless bit.readable_by? @user end 55 | # Kick out any users which do not have write access to a certain resource. 56 | def only_can_write bit; raise ParkPlace::AccessDenied unless bit.writable_by? @user end 57 | # Kick out any users which do not own a certain resource. 58 | def only_owner_of bit; raise ParkPlace::AccessDenied unless bit.owned_by? @user end 59 | # Kick out any non-superusers 60 | def only_superusers; raise ParkPlace::AccessDenied unless @user.superuser? end 61 | 62 | # Build an ActiveRecord Errors set. 63 | def error(msg) 64 | @state.next_errors = ActiveRecord::Errors.new(@state).instance_eval do 65 | add_to_base msg 66 | self 67 | end 68 | end 69 | end 70 | 71 | module ParkPlace::S3 72 | # This method overrides Camping's own service method. The idea here is 73 | # to set up some common instance vars and check authentication. Here's the rundown: 74 | # 75 | # # The @meta variable is setup, containing any metadata headers 76 | # (starting with x-amz-meta-.) 77 | # # Authorization is checked. If a Signature is found in the URL string, it 78 | # is used. Otherwise, the Authorization HTTP header is used. 79 | # # If authorization is successful, the @user variable contains a valid User 80 | # object. If not, @user is nil. 81 | # 82 | # If a ParkPlace exception is thrown (anything derived from ParkPlace::ServiceError), 83 | # the exception is displayed as XML. 84 | def service(*a) 85 | @meta, @amz = ParkPlace::H[], ParkPlace::H[] 86 | @env.each do |k, v| 87 | k = k.downcase.gsub('_', '-') 88 | @amz[$1] = v.strip if k =~ /^http-x-amz-([-\w]+)$/ 89 | @meta[$1] = v if k =~ /^http-x-amz-meta-([-\w]+)$/ 90 | end 91 | 92 | auth, key_s, secret_s = *@env.HTTP_AUTHORIZATION.to_s.match(/^AWS (\w+):(.+)$/) 93 | date_s = @env.HTTP_X_AMZ_DATE || @env.HTTP_DATE 94 | if @input.Signature and Time.at(@input.Expires.to_i) >= Time.now 95 | key_s, secret_s, date_s = @input.AWSAccessKeyId, @input.Signature, @input.Expires 96 | end 97 | uri = @env.PATH_INFO 98 | uri += "?" + @env.QUERY_STRING if ParkPlace::RESOURCE_TYPES.include?(@env.QUERY_STRING) 99 | canonical = [@env.REQUEST_METHOD, @env.HTTP_CONTENT_MD5, @env.HTTP_CONTENT_TYPE, 100 | date_s, uri] 101 | @amz.sort.each do |k, v| 102 | canonical[-1,0] = "x-amz-#{k}:#{v}" 103 | end 104 | @user = ParkPlace::Models::User.find_by_key key_s 105 | if @user and secret_s != hmac_sha1(@user.secret, canonical.map{|v|v.to_s.strip} * "\n") 106 | raise BadAuthentication 107 | end 108 | 109 | s = super(*a) 110 | s.headers['Server'] = 'ParkPlace' 111 | s 112 | rescue ParkPlace::ServiceError => e 113 | xml e.status do |x| 114 | x.Error do 115 | x.Code e.code 116 | x.Message e.message 117 | x.Resource @env.PATH_INFO 118 | x.RequestId Time.now.to_i 119 | end 120 | end 121 | self 122 | end 123 | 124 | # Parse any ACL requests which have come in. 125 | def requested_acl 126 | # FIX: parse XML 127 | raise NotImplemented if @input.has_key? 'acl' 128 | {:access => ParkPlace::CANNED_ACLS[@amz['acl']] || ParkPlace::CANNED_ACLS['private']} 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/parkplace/mimetypes_hash.rb: -------------------------------------------------------------------------------- 1 | MIME_TYPES = { 2 | ".rpm" => "application/x-rpm", 3 | ".pdf" => "application/pdf", 4 | ".sig" => "application/pgp-signature", 5 | ".spl" => "application/futuresplash", 6 | ".class" => "application/octet-stream", 7 | ".ps" => "application/postscript", 8 | ".torrent" => "application/x-bittorrent", 9 | ".dvi" => "application/x-dvi", 10 | ".gz" => "application/x-gzip", 11 | ".pac" => "application/x-ns-proxy-autoconfig", 12 | ".swf" => "application/x-shockwave-flash", 13 | ".tar.gz" => "application/x-tgz", 14 | ".tgz" => "application/x-tgz", 15 | ".tar" => "application/x-tar", 16 | ".zip" => "application/zip", 17 | ".mp3" => "audio/mpeg", 18 | ".m3u" => "audio/x-mpegurl", 19 | ".wma" => "audio/x-ms-wma", 20 | ".wax" => "audio/x-ms-wax", 21 | ".ogg" => "audio/x-wav", 22 | ".wav" => "audio/x-wav", 23 | ".gif" => "image/gif", 24 | ".jpg" => "image/jpeg", 25 | ".jpeg" => "image/jpeg", 26 | ".png" => "image/png", 27 | ".xbm" => "image/x-xbitmap", 28 | ".xpm" => "image/x-xpixmap", 29 | ".xwd" => "image/x-xwindowdump", 30 | ".css" => "text/css", 31 | ".html" => "text/html", 32 | ".htm" => "text/html", 33 | ".js" => "text/javascript", 34 | ".asc" => "text/plain", 35 | ".c" => "text/plain", 36 | ".conf" => "text/plain", 37 | ".text" => "text/plain", 38 | ".txt" => "text/plain", 39 | ".dtd" => "text/xml", 40 | ".xml" => "text/xml", 41 | ".mpeg" => "video/mpeg", 42 | ".mpg" => "video/mpeg", 43 | ".mov" => "video/quicktime", 44 | ".qt" => "video/quicktime", 45 | ".avi" => "video/x-msvideo", 46 | ".asf" => "video/x-ms-asf", 47 | ".asx" => "video/x-ms-asf", 48 | ".wmv" => "video/x-ms-wmv", 49 | ".bz2" => "application/x-bzip", 50 | ".tbz" => "application/x-bzip-compressed-tar", 51 | ".tar.bz2" => "application/x-bzip-compressed-tar" 52 | } 53 | -------------------------------------------------------------------------------- /lib/parkplace/models.rb: -------------------------------------------------------------------------------- 1 | module ParkPlace::Models 2 | 3 | class FileInfo 4 | attr_accessor :path, :mime_type, :disposition, :size, :md5 5 | end 6 | 7 | class User < Base 8 | has_many :bits, :foreign_key => 'owner_id' 9 | validates_length_of :login, :within => 3..40 10 | validates_uniqueness_of :login 11 | validates_uniqueness_of :key 12 | validates_presence_of :password 13 | validates_confirmation_of :password 14 | def before_save 15 | @password_clean = self.password 16 | self.password = hmac_sha1(self.password, self.secret) 17 | end 18 | def after_save 19 | self.password = @password_clean 20 | end 21 | end 22 | 23 | class Bit < Base 24 | acts_as_nested_set 25 | serialize :meta 26 | serialize :obj 27 | belongs_to :owner, :class_name => 'User', :foreign_key => 'owner_id' 28 | has_and_belongs_to_many :users 29 | has_one :torrent 30 | validates_length_of :name, :within => 3..255 31 | 32 | def fullpath; File.join(STORAGE_PATH, name) end 33 | def grant hsh 34 | if hsh[:access] 35 | self.access = hsh[:access] 36 | self.save 37 | end 38 | end 39 | def access_readable 40 | name, _ = CANNED_ACLS.find { |k, v| v == self.access } 41 | if name 42 | name 43 | else 44 | [0100, 0010, 0001].map do |i| 45 | [[4, 'r'], [2, 'w'], [1, 'x']].map do |k, v| 46 | (self.access & (i * k) == 0 ? '-' : v ) 47 | end 48 | end.join 49 | end 50 | end 51 | def check_access user, group_perm, user_perm 52 | !!( if owned_by?(user) or (user and access & group_perm > 0) or (access & user_perm > 0) 53 | true 54 | elsif user 55 | acl = users.find(user.id) rescue nil 56 | acl and acl.access.to_i & user_perm 57 | end ) 58 | end 59 | def owned_by? user 60 | user and owner_id == user.id 61 | end 62 | def readable_by? user 63 | check_access(user, READABLE_BY_AUTH, READABLE) 64 | end 65 | def writable_by? user 66 | check_access(user, WRITABLE_BY_AUTH, WRITABLE) 67 | end 68 | end 69 | 70 | class Bucket < Bit 71 | validates_format_of :name, :with => /^[-\w]+$/ 72 | def self.find_root(bucket_name) 73 | find(:first, :conditions => ['parent_id IS NULL AND name = ?', bucket_name]) or raise NoSuchBucket 74 | end 75 | def find_slot(oid) 76 | Slot.find(:first, :conditions => ['parent_id = ? AND name = ?', self.id, oid]) or raise NoSuchKey 77 | end 78 | end 79 | 80 | class Slot < Bit 81 | def fullpath; File.join(STORAGE_PATH, obj.path) end 82 | def etag 83 | if self.obj.respond_to? :md5 84 | self.obj.md5 85 | else 86 | %{"#{MD5.md5(self.obj)}"} 87 | end 88 | end 89 | end 90 | 91 | class Torrent < Base 92 | belongs_to :bit 93 | has_many :torrent_peers 94 | end 95 | 96 | class TorrentPeer < Base 97 | belongs_to :torrent 98 | end 99 | 100 | class SetupParkPlace < V 1.0 101 | def self.up 102 | create_table :parkplace_bits do |t| 103 | t.column :id, :integer, :null => false 104 | t.column :owner_id, :integer 105 | t.column :parent_id, :integer 106 | t.column :lft, :integer 107 | t.column :rgt, :integer 108 | t.column :type, :string, :limit => 6 109 | t.column :name, :string, :limit => 255 110 | t.column :created_at, :timestamp 111 | t.column :updated_at, :timestamp 112 | t.column :access, :integer 113 | t.column :meta, :text 114 | t.column :obj, :text 115 | end 116 | create_table :parkplace_users do |t| 117 | t.column :id, :integer, :null => false 118 | t.column :login, :string, :limit => 40 119 | t.column :password, :string, :limit => 40 120 | t.column :email, :string, :limit => 64 121 | t.column :key, :string, :limit => 64 122 | t.column :secret, :string, :limit => 64 123 | t.column :created_at, :datetime 124 | t.column :activated_at, :datetime 125 | t.column :superuser, :integer, :default => 0 126 | t.column :deleted, :integer, :default => 0 127 | end 128 | create_table :parkplace_bits_users do |t| 129 | t.column :bit_id, :integer 130 | t.column :user_id, :integer 131 | t.column :access, :integer 132 | end 133 | create_table :parkplace_torrents do |t| 134 | t.column :id, :integer, :null => false 135 | t.column :bit_id, :integer 136 | t.column :info_hash, :string, :limit => 40 137 | t.column :metainfo, :binary 138 | t.column :seeders, :integer, :null => false, :default => 0 139 | t.column :leechers, :integer, :null => false, :default => 0 140 | t.column :hits, :integer, :null => false, :default => 0 141 | t.column :total, :integer, :null => false, :default => 0 142 | t.column :updated_at, :timestamp 143 | end 144 | create_table :parkplace_torrent_peers do |t| 145 | t.column :id, :integer, :null => false 146 | t.column :torrent_id, :integer 147 | t.column :guid, :string, :limit => 40 148 | t.column :ipaddr, :string 149 | t.column :port, :integer 150 | t.column :uploaded, :integer, :null => false, :default => 0 151 | t.column :downloaded, :integer, :null => false, :default => 0 152 | t.column :remaining, :integer, :null => false, :default => 0 153 | t.column :compact, :integer, :null => false, :default => 0 154 | t.column :event, :integer, :null => false, :default => 0 155 | t.column :key, :string, :limit => 55 156 | t.column :created_at, :timestamp 157 | t.column :updated_at, :timestamp 158 | end 159 | end 160 | def self.down 161 | drop_table :parkplace_bits 162 | drop_table :parkplace_users 163 | drop_table :parkplace_bits_users 164 | drop_table :parkplace_torrents 165 | drop_table :parkplace_torrent_peers 166 | end 167 | end 168 | 169 | end 170 | -------------------------------------------------------------------------------- /lib/parkplace/s3.rb: -------------------------------------------------------------------------------- 1 | module ParkPlace::Controllers 2 | class RService < S3 '/' 3 | def get 4 | only_authorized 5 | buckets = Bucket.find :all, :conditions => ['parent_id IS NULL AND owner_id = ?', @user.id], :order => "name" 6 | 7 | xml do |x| 8 | x.ListAllMyBucketsResult :xmlns => "http://s3.amazonaws.com/doc/2006-03-01/" do 9 | x.Owner do 10 | x.ID @user.key 11 | x.DisplayName @user.login 12 | end 13 | x.Buckets do 14 | buckets.each do |b| 15 | x.Bucket do 16 | x.Name b.name 17 | x.CreationDate b.created_at.getgm.iso8601 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | 26 | class RBucket < S3 '/([^\/]+)/?' 27 | def put(bucket_name) 28 | only_authorized 29 | bucket = Bucket.find_root(bucket_name) 30 | only_owner_of bucket 31 | bucket.grant(requested_acl) 32 | r(200, '', 'Location' => @env.PATH_INFO, 'Content-Length' => 0) 33 | rescue NoSuchBucket 34 | Bucket.create(:name => bucket_name, :owner_id => @user.id).grant(requested_acl) 35 | r(200, '', 'Location' => @env.PATH_INFO, 'Content-Length' => 0) 36 | end 37 | def delete(bucket_name) 38 | bucket = Bucket.find_root(bucket_name) 39 | only_owner_of bucket 40 | 41 | if Slot.count(:conditions => ['parent_id = ?', bucket.id]) > 0 42 | raise BucketNotEmpty 43 | end 44 | bucket.destroy 45 | r(204, '') 46 | end 47 | def get(bucket_name) 48 | bucket = Bucket.find_root(bucket_name) 49 | only_can_read bucket 50 | 51 | if @input.has_key? 'torrent' 52 | return torrent(bucket) 53 | end 54 | 55 | # Patch from Alan Wootton -- used to be ticket 12 in Trac: 56 | # Let's be more like amazon and always have these 3 things 57 | @input['max-keys'] = 1000 unless @input['max-keys'] 58 | @input.marker = '' unless @input.marker 59 | @input.prefix = '' unless @input.prefix 60 | 61 | opts = {:conditions => ['parent_id = ?', bucket.id], :order => "name", :include => :owner} 62 | 63 | if @input.prefix && @input.prefix.length > 0 64 | opts[:conditions].first << ' AND name LIKE ?' 65 | opts[:conditions] << "#{@input.prefix}%" 66 | end 67 | opts[:offset] = 0 68 | if @input.marker && @input.marker.length > 0 69 | opts[:conditions].first << ' AND name > ?' 70 | opts[:conditions] << "#{@input.marker}" 71 | end 72 | if @input['max-keys'] 73 | opts[:limit] = @input['max-keys'].to_i 74 | end 75 | slot_count = Slot.count :conditions => opts[:conditions] 76 | contents = Slot.find :all, opts 77 | 78 | if @input.delimiter 79 | @input.prefix = '' if @input.prefix.nil? 80 | 81 | # Build a hash of { :prefix => content_key }. The prefix will not include the supplied @input.prefix. 82 | prefixes = contents.inject({}) do |hash, c| 83 | prefix = get_prefix(c).to_sym 84 | hash[prefix] = [] unless hash[prefix] 85 | hash[prefix] << c.name 86 | hash 87 | end 88 | 89 | # The common prefixes are those with more than one element 90 | common_prefixes = prefixes.inject([]) do |array, prefix| 91 | array << prefix[0].to_s if prefix[1].size > 1 92 | array 93 | end 94 | 95 | # The contents are everything that doesn't have a common prefix 96 | contents = contents.reject do |c| 97 | common_prefixes.include? get_prefix(c) 98 | end 99 | end 100 | 101 | xml do |x| 102 | x.ListBucketResult :xmlns => "http://s3.amazonaws.com/doc/2006-03-01/" do 103 | x.Name bucket.name 104 | x.Prefix @input.prefix if @input.prefix 105 | x.Marker @input.marker if @input.marker 106 | x.Delimiter @input.delimiter if @input.delimiter 107 | x.MaxKeys @input['max-keys'] if @input['max-keys'] 108 | x.IsTruncated slot_count > contents.length + opts[:offset].to_i 109 | contents.each do |c| 110 | x.Contents do 111 | x.Key c.name 112 | x.LastModified c.updated_at.getgm.iso8601 113 | x.ETag c.etag 114 | x.Size c.obj.size 115 | x.StorageClass "STANDARD" 116 | x.Owner do 117 | x.ID c.owner.key 118 | x.DisplayName c.owner.login 119 | end 120 | end 121 | end 122 | if common_prefixes 123 | common_prefixes.each do |p| 124 | x.CommonPrefixes do 125 | x.Prefix p 126 | end 127 | end 128 | end 129 | end 130 | end 131 | end 132 | 133 | private 134 | def get_prefix(c) 135 | c.name.sub(@input.prefix, '').split(@input.delimiter)[0] + @input.delimiter 136 | end 137 | end 138 | 139 | class RSlot < S3 '/(.+?)/(.+)' 140 | include ParkPlace::S3, ParkPlace::SlotGet 141 | def put(bucket_name, oid) 142 | bucket = Bucket.find_root bucket_name 143 | only_can_write bucket 144 | raise MissingContentLength unless @env.HTTP_CONTENT_LENGTH 145 | 146 | if @env.HTTP_X_AMZ_COPY_SOURCE.to_s =~ /\/(.+?)\/(.+)/ 147 | source_bucket_name = $1 148 | source_oid = $2 149 | 150 | source_slot = ParkPlace::Models::Bucket.find_root(source_bucket_name).find_slot(source_oid) 151 | only_can_read source_slot 152 | 153 | fileinfo = FileInfo.new 154 | [:mime_type, :disposition, :size, :md5].each { |a| fileinfo.send("#{a}=", source_slot.obj.send(a)) } 155 | fileinfo.path = File.join(bucket_name, rand(10000).to_s(36) + '_' + File.basename(source_slot.obj.path)) 156 | fileinfo.path.succ! while File.exists?(File.join(STORAGE_PATH, fileinfo.path)) 157 | file_path = File.join(STORAGE_PATH, fileinfo.path) 158 | FileUtils.mkdir_p(File.dirname(file_path)) 159 | FileUtils.cp(File.join(STORAGE_PATH, source_slot.obj.path), file_path) 160 | else 161 | temp_path = @in.path rescue nil 162 | readlen = 0 163 | md5 = MD5.new 164 | Tempfile.open(File.basename(oid)) do |tmpf| 165 | temp_path ||= tmpf.path 166 | tmpf.binmode 167 | while part = @in.read(BUFSIZE) 168 | readlen += part.size 169 | md5 << part 170 | tmpf << part unless @in.is_a?(Tempfile) 171 | end 172 | end 173 | 174 | fileinfo = FileInfo.new 175 | fileinfo.mime_type = @env.HTTP_CONTENT_TYPE || "binary/octet-stream" 176 | fileinfo.disposition = @env.HTTP_CONTENT_DISPOSITION 177 | fileinfo.size = readlen 178 | fileinfo.md5 = Base64.encode64(md5.digest).strip 179 | 180 | raise IncompleteBody if @env.HTTP_CONTENT_LENGTH.to_i != readlen 181 | if @env.HTTP_CONTENT_MD5 182 | b64cs = /[0-9a-zA-Z+\/]/ 183 | re = / 184 | ^ 185 | (?:#{b64cs}{4})* # any four legal chars 186 | (?:#{b64cs}{2} # right-padded by up to two =s 187 | (?:#{b64cs}|=){2})? 188 | $ 189 | /ox 190 | 191 | raise InvalidDigest unless @env.HTTP_CONTENT_MD5 =~ re 192 | raise BadDigest unless fileinfo.md5 == @env.HTTP_CONTENT_MD5 193 | end 194 | 195 | fileinfo.path = File.join(bucket_name, rand(10000).to_s(36) + '_' + File.basename(temp_path)) 196 | fileinfo.path.succ! while File.exists?(File.join(STORAGE_PATH, fileinfo.path)) 197 | file_path = File.join(STORAGE_PATH, fileinfo.path) 198 | FileUtils.mkdir_p(File.dirname(file_path)) 199 | FileUtils.mv(temp_path, file_path) 200 | end 201 | 202 | slot = nil 203 | meta = @meta.empty? ? nil : {}.merge(@meta) 204 | owner_id = @user ? @user.id : bucket.owner_id 205 | begin 206 | slot = bucket.find_slot(oid) 207 | prev_path = slot.obj.path 208 | slot.update_attributes(:owner_id => owner_id, :meta => meta, :obj => fileinfo) 209 | # Remove the old file instead of leaving it lying around. 210 | FileUtils.rm(File.join(STORAGE_PATH,prev_path)) unless prev_path == fileinfo.path 211 | rescue NoSuchKey 212 | slot = Slot.create(:name => oid, :owner_id => owner_id, :meta => meta, :obj => fileinfo) 213 | bucket.add_child(slot) 214 | end 215 | slot.grant(requested_acl) 216 | r(200, '', 'ETag' => slot.etag, 'Content-Length' => 0) 217 | end 218 | def delete(bucket_name, oid) 219 | bucket = Bucket.find_root bucket_name 220 | only_can_write bucket 221 | @slot = bucket.find_slot(oid) 222 | @slot.destroy 223 | r(204, '') 224 | rescue NoSuchKey 225 | r(204, '') 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /lib/parkplace/torrent.rb: -------------------------------------------------------------------------------- 1 | require 'rubytorrent' 2 | 3 | class String 4 | def to_hex_s 5 | unpack("H*").first 6 | end 7 | def from_hex_s 8 | [self].pack("H*") 9 | end 10 | end 11 | 12 | module ParkPlace 13 | TRACKER_INTERVAL = 10.minutes 14 | 15 | # All tracker errors are thrown as this class. 16 | class TrackerError < Exception; end 17 | 18 | def torrent bit 19 | mi = bit.metainfo 20 | mi.announce = URI("http:#{URL(Controllers::CTracker)}") 21 | mi.created_by = "Served by ParkPlace/#{ParkPlace::VERSION}" 22 | mi.creation_date = Time.now 23 | t = Models::Torrent.find_by_bit_id bit.id 24 | info_hash = Digest::SHA1.digest(mi.info.to_bencoding).to_hex_s 25 | unless t and t.info_hash == info_hash 26 | t ||= Models::Torrent.new 27 | t.update_attributes(:info_hash => info_hash, :bit_id => bit.id, :metainfo => mi.to_bencoding) 28 | end 29 | r(200, mi.to_bencoding, 'Content-Disposition' => "attachment; filename=#{bit.name}.torrent;", 30 | 'Content-Type' => 'application/x-bittorrent') 31 | end 32 | 33 | def torrent_list(info_hash) 34 | params = {:order => 'seeders DESC, leechers DESC', :include => :bit} 35 | if info_hash 36 | params[:conditions] = ['info_hash = ?', info_hash] 37 | end 38 | Models::Torrent.find :all, params 39 | end 40 | 41 | def tracker_reply(params) 42 | r(200, params.merge('interval' => TRACKER_INTERVAL).to_bencoding, 'Content-Type' => 'text/plain') 43 | end 44 | 45 | def tracker_error msg 46 | r(200, {'failure reason' => msg}.to_bencoding, 'Content-Type' => 'text/plain') 47 | end 48 | end 49 | 50 | module ParkPlace::Models 51 | class Bit 52 | def each_piece(files, length) 53 | buf = "" 54 | files.each do |f| 55 | File.open(f) do |fh| 56 | begin 57 | read = fh.read(length - buf.length) 58 | if (buf.length + read.length) == length 59 | yield(buf + read) 60 | buf = "" 61 | else 62 | buf += read 63 | end 64 | end until fh.eof? 65 | end 66 | end 67 | 68 | yield buf 69 | end 70 | end 71 | 72 | class Bucket 73 | def metainfo 74 | children = self.all_children 75 | mii = RubyTorrent::MetaInfoInfo.new 76 | mii.name = self.name 77 | mii.piece_length = 512.kilobytes 78 | mii.files, files = [], [] 79 | mii.pieces = "" 80 | i = 0 81 | Slot.find(:all, :conditions => ['parent_id = ?', self.id]).each do |slot| 82 | miif = RubyTorrent::MetaInfoInfoFile.new 83 | miif.length = slot.obj.size 84 | miif.md5sum = slot.obj.md5 85 | miif.path = File.split(slot.name) 86 | mii.files << miif 87 | files << slot.fullpath 88 | end 89 | each_piece(files, mii.piece_length) do |piece| 90 | mii.pieces += Digest::SHA1.digest(piece) 91 | i += 1 92 | end 93 | mi = RubyTorrent::MetaInfo.new 94 | mi.info = mii 95 | mi 96 | end 97 | end 98 | 99 | class Slot 100 | def metainfo 101 | mii = RubyTorrent::MetaInfoInfo.new 102 | mii.name = self.name 103 | mii.length = self.obj.size 104 | mii.md5sum = self.obj.md5 105 | mii.piece_length = 512.kilobytes 106 | mii.pieces = "" 107 | i = 0 108 | each_piece([self.fullpath], mii.piece_length) do |piece| 109 | mii.pieces += Digest::SHA1.digest(piece) 110 | i += 1 111 | end 112 | mi = RubyTorrent::MetaInfo.new 113 | mi.info = mii 114 | mi 115 | end 116 | end 117 | end 118 | 119 | module ParkPlace::Controllers 120 | class CTracker < R '/tracker/announce' 121 | EVENT_CODES = { 122 | 'started' => 200, 123 | 'completed' => 201, 124 | 'stopped' => 202 125 | } 126 | def get 127 | raise ParkPlace::TrackerError, "No info_hash present." unless @input.info_hash 128 | raise ParkPlace::TrackerError, "No peer_id present." unless @input.peer_id 129 | 130 | # p @input 131 | info_hash = @input.info_hash.to_hex_s 132 | guid = @input.peer_id.to_hex_s 133 | trnt = Torrent.find_by_info_hash(info_hash) 134 | raise ParkPlace::TrackerError, "No file found with hash of `#{@input.info_hash}'." unless trnt 135 | 136 | peer = TorrentPeer.find_by_guid_and_torrent_id(guid, trnt.id) 137 | unless peer 138 | peer = TorrentPeer.find_by_ipaddr_and_port_and_torrent_id(@env.REMOTE_ADDR, @input.port, trnt.id) 139 | end 140 | unless peer 141 | peer = TorrentPeer.new(:torrent_id => trnt.id) 142 | trnt.hits += 1 143 | end 144 | 145 | if @input.event == 'completed' 146 | trnt.total += 1 147 | end 148 | @input.event = 'completed' if @input.left == "0" 149 | if @input.event 150 | peer.update_attributes(:uploaded => @input.uploaded, :downloaded => @input.downloaded, 151 | :remaining => @input.left, :event => EVENT_CODES[@input.event], :key => @input.key, 152 | :port => @input.port, :ipaddr => @env.REMOTE_ADDR, :guid => guid) 153 | end 154 | complete, incomplete = 0, 0 155 | peers = trnt.torrent_peers.map do |peer| 156 | if peer.updated_at < Time.now - (TRACKER_INTERVAL * 2) or (@input.event == 'stopped' and peer.guid == guid) 157 | peer.destroy 158 | next 159 | end 160 | if peer.event == EVENT_CODES['completed'] 161 | complete += 1 162 | else 163 | incomplete += 1 164 | end 165 | next if peer.guid == guid 166 | {'peer id' => peer.guid.from_hex_s, 'ip' => peer.ipaddr, 'port' => peer.port} 167 | end.compact 168 | trnt.seeders = complete 169 | trnt.leechers = incomplete 170 | trnt.save 171 | tracker_reply('peers' => peers, 'complete' => complete, 'incomplete' => incomplete) 172 | rescue Exception => e 173 | puts "#{e.class}: #{e.message}" 174 | tracker_error "#{e.class}: #{e.message}" 175 | end 176 | end 177 | 178 | class CTrackerScrape < R '/tracker/scrape' 179 | def get 180 | torrents = torrent_list @input.info_hash 181 | tracker_reply('files' => torrents.map { |t| 182 | {'complete' => t.seeders, 'downloaded' => t.total, 'incomplete' => t.leechers, 'name' => t.bit.name} }) 183 | end 184 | end 185 | 186 | class CTrackerIndex < R '/tracker' 187 | def get 188 | @torrents = torrent_list @input.info_hash 189 | @transfer = TorrentPeer.sum :downloaded, :group => :torrent 190 | render :torrent_index 191 | end 192 | end 193 | end 194 | 195 | module ParkPlace::Views 196 | def torrent_index 197 | html do 198 | head do 199 | title "Park Place Torrents" 200 | end 201 | body do 202 | table do 203 | thead do 204 | tr do 205 | th "Name" 206 | th "Size" 207 | th "Seeders" 208 | th "Leechers" 209 | th "Downloads" 210 | th "Transferred" 211 | th "Since" 212 | end 213 | end 214 | tbody do 215 | torrents.each do |t| 216 | tr do 217 | th t.bit.name 218 | td number_to_human_size(t.bit.obj.size) 219 | td t.seeders 220 | td t.leechers 221 | td t.total 222 | td number_to_human_size(@transfer[t]) 223 | # td t.metainfo.creation_date 224 | end 225 | end 226 | end 227 | end 228 | end 229 | end 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /setup.rb: -------------------------------------------------------------------------------- 1 | # 2 | # setup.rb 3 | # 4 | # Copyright (c) 2000-2005 Minero Aoki 5 | # 6 | # This program is free software. 7 | # You can distribute/modify this program under the terms of 8 | # the GNU LGPL, Lesser General Public License version 2.1. 9 | # 10 | 11 | unless Enumerable.method_defined?(:map) # Ruby 1.4.6 12 | module Enumerable 13 | alias map collect 14 | end 15 | end 16 | 17 | unless File.respond_to?(:read) # Ruby 1.6 18 | def File.read(fname) 19 | open(fname) {|f| 20 | return f.read 21 | } 22 | end 23 | end 24 | 25 | unless Errno.const_defined?(:ENOTEMPTY) # Windows? 26 | module Errno 27 | class ENOTEMPTY 28 | # We do not raise this exception, implementation is not needed. 29 | end 30 | end 31 | end 32 | 33 | def File.binread(fname) 34 | open(fname, 'rb') {|f| 35 | return f.read 36 | } 37 | end 38 | 39 | # for corrupted Windows' stat(2) 40 | def File.dir?(path) 41 | File.directory?((path[-1,1] == '/') ? path : path + '/') 42 | end 43 | 44 | 45 | class ConfigTable 46 | 47 | include Enumerable 48 | 49 | def initialize(rbconfig) 50 | @rbconfig = rbconfig 51 | @items = [] 52 | @table = {} 53 | # options 54 | @install_prefix = nil 55 | @config_opt = nil 56 | @verbose = true 57 | @no_harm = false 58 | end 59 | 60 | attr_accessor :install_prefix 61 | attr_accessor :config_opt 62 | 63 | attr_writer :verbose 64 | 65 | def verbose? 66 | @verbose 67 | end 68 | 69 | attr_writer :no_harm 70 | 71 | def no_harm? 72 | @no_harm 73 | end 74 | 75 | def [](key) 76 | lookup(key).resolve(self) 77 | end 78 | 79 | def []=(key, val) 80 | lookup(key).set val 81 | end 82 | 83 | def names 84 | @items.map {|i| i.name } 85 | end 86 | 87 | def each(&block) 88 | @items.each(&block) 89 | end 90 | 91 | def key?(name) 92 | @table.key?(name) 93 | end 94 | 95 | def lookup(name) 96 | @table[name] or setup_rb_error "no such config item: #{name}" 97 | end 98 | 99 | def add(item) 100 | @items.push item 101 | @table[item.name] = item 102 | end 103 | 104 | def remove(name) 105 | item = lookup(name) 106 | @items.delete_if {|i| i.name == name } 107 | @table.delete_if {|name, i| i.name == name } 108 | item 109 | end 110 | 111 | def load_script(path, inst = nil) 112 | if File.file?(path) 113 | MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path 114 | end 115 | end 116 | 117 | def savefile 118 | '.config' 119 | end 120 | 121 | def load_savefile 122 | begin 123 | File.foreach(savefile()) do |line| 124 | k, v = *line.split(/=/, 2) 125 | self[k] = v.strip 126 | end 127 | rescue Errno::ENOENT 128 | setup_rb_error $!.message + "\n#{File.basename($0)} config first" 129 | end 130 | end 131 | 132 | def save 133 | @items.each {|i| i.value } 134 | File.open(savefile(), 'w') {|f| 135 | @items.each do |i| 136 | f.printf "%s=%s\n", i.name, i.value if i.value? and i.value 137 | end 138 | } 139 | end 140 | 141 | def load_standard_entries 142 | standard_entries(@rbconfig).each do |ent| 143 | add ent 144 | end 145 | end 146 | 147 | def standard_entries(rbconfig) 148 | c = rbconfig 149 | 150 | rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) 151 | 152 | major = c['MAJOR'].to_i 153 | minor = c['MINOR'].to_i 154 | teeny = c['TEENY'].to_i 155 | version = "#{major}.#{minor}" 156 | 157 | # ruby ver. >= 1.4.4? 158 | newpath_p = ((major >= 2) or 159 | ((major == 1) and 160 | ((minor >= 5) or 161 | ((minor == 4) and (teeny >= 4))))) 162 | 163 | if c['rubylibdir'] 164 | # V > 1.6.3 165 | libruby = "#{c['prefix']}/lib/ruby" 166 | librubyver = c['rubylibdir'] 167 | librubyverarch = c['archdir'] 168 | siteruby = c['sitedir'] 169 | siterubyver = c['sitelibdir'] 170 | siterubyverarch = c['sitearchdir'] 171 | elsif newpath_p 172 | # 1.4.4 <= V <= 1.6.3 173 | libruby = "#{c['prefix']}/lib/ruby" 174 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 175 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 176 | siteruby = c['sitedir'] 177 | siterubyver = "$siteruby/#{version}" 178 | siterubyverarch = "$siterubyver/#{c['arch']}" 179 | else 180 | # V < 1.4.4 181 | libruby = "#{c['prefix']}/lib/ruby" 182 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 183 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 184 | siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" 185 | siterubyver = siteruby 186 | siterubyverarch = "$siterubyver/#{c['arch']}" 187 | end 188 | parameterize = lambda {|path| 189 | path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') 190 | } 191 | 192 | if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } 193 | makeprog = arg.sub(/'/, '').split(/=/, 2)[1] 194 | else 195 | makeprog = 'make' 196 | end 197 | 198 | [ 199 | ExecItem.new('installdirs', 'std/site/home', 200 | 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ 201 | {|val, table| 202 | case val 203 | when 'std' 204 | table['rbdir'] = '$librubyver' 205 | table['sodir'] = '$librubyverarch' 206 | when 'site' 207 | table['rbdir'] = '$siterubyver' 208 | table['sodir'] = '$siterubyverarch' 209 | when 'home' 210 | setup_rb_error '$HOME was not set' unless ENV['HOME'] 211 | table['prefix'] = ENV['HOME'] 212 | table['rbdir'] = '$libdir/ruby' 213 | table['sodir'] = '$libdir/ruby' 214 | end 215 | }, 216 | PathItem.new('prefix', 'path', c['prefix'], 217 | 'path prefix of target environment'), 218 | PathItem.new('bindir', 'path', parameterize.call(c['bindir']), 219 | 'the directory for commands'), 220 | PathItem.new('libdir', 'path', parameterize.call(c['libdir']), 221 | 'the directory for libraries'), 222 | PathItem.new('datadir', 'path', parameterize.call(c['datadir']), 223 | 'the directory for shared data'), 224 | PathItem.new('mandir', 'path', parameterize.call(c['mandir']), 225 | 'the directory for man pages'), 226 | PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), 227 | 'the directory for system configuration files'), 228 | PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), 229 | 'the directory for local state data'), 230 | PathItem.new('libruby', 'path', libruby, 231 | 'the directory for ruby libraries'), 232 | PathItem.new('librubyver', 'path', librubyver, 233 | 'the directory for standard ruby libraries'), 234 | PathItem.new('librubyverarch', 'path', librubyverarch, 235 | 'the directory for standard ruby extensions'), 236 | PathItem.new('siteruby', 'path', siteruby, 237 | 'the directory for version-independent aux ruby libraries'), 238 | PathItem.new('siterubyver', 'path', siterubyver, 239 | 'the directory for aux ruby libraries'), 240 | PathItem.new('siterubyverarch', 'path', siterubyverarch, 241 | 'the directory for aux ruby binaries'), 242 | PathItem.new('rbdir', 'path', '$siterubyver', 243 | 'the directory for ruby scripts'), 244 | PathItem.new('sodir', 'path', '$siterubyverarch', 245 | 'the directory for ruby extentions'), 246 | PathItem.new('rubypath', 'path', rubypath, 247 | 'the path to set to #! line'), 248 | ProgramItem.new('rubyprog', 'name', rubypath, 249 | 'the ruby program using for installation'), 250 | ProgramItem.new('makeprog', 'name', makeprog, 251 | 'the make program to compile ruby extentions'), 252 | SelectItem.new('shebang', 'all/ruby/never', 'ruby', 253 | 'shebang line (#!) editing mode'), 254 | BoolItem.new('without-ext', 'yes/no', 'no', 255 | 'does not compile/install ruby extentions') 256 | ] 257 | end 258 | private :standard_entries 259 | 260 | def load_multipackage_entries 261 | multipackage_entries().each do |ent| 262 | add ent 263 | end 264 | end 265 | 266 | def multipackage_entries 267 | [ 268 | PackageSelectionItem.new('with', 'name,name...', '', 'ALL', 269 | 'package names that you want to install'), 270 | PackageSelectionItem.new('without', 'name,name...', '', 'NONE', 271 | 'package names that you do not want to install') 272 | ] 273 | end 274 | private :multipackage_entries 275 | 276 | ALIASES = { 277 | 'std-ruby' => 'librubyver', 278 | 'stdruby' => 'librubyver', 279 | 'rubylibdir' => 'librubyver', 280 | 'archdir' => 'librubyverarch', 281 | 'site-ruby-common' => 'siteruby', # For backward compatibility 282 | 'site-ruby' => 'siterubyver', # For backward compatibility 283 | 'bin-dir' => 'bindir', 284 | 'bin-dir' => 'bindir', 285 | 'rb-dir' => 'rbdir', 286 | 'so-dir' => 'sodir', 287 | 'data-dir' => 'datadir', 288 | 'ruby-path' => 'rubypath', 289 | 'ruby-prog' => 'rubyprog', 290 | 'ruby' => 'rubyprog', 291 | 'make-prog' => 'makeprog', 292 | 'make' => 'makeprog' 293 | } 294 | 295 | def fixup 296 | ALIASES.each do |ali, name| 297 | @table[ali] = @table[name] 298 | end 299 | @items.freeze 300 | @table.freeze 301 | @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ 302 | end 303 | 304 | def parse_opt(opt) 305 | m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" 306 | m.to_a[1,2] 307 | end 308 | 309 | def dllext 310 | @rbconfig['DLEXT'] 311 | end 312 | 313 | def value_config?(name) 314 | lookup(name).value? 315 | end 316 | 317 | class Item 318 | def initialize(name, template, default, desc) 319 | @name = name.freeze 320 | @template = template 321 | @value = default 322 | @default = default 323 | @description = desc 324 | end 325 | 326 | attr_reader :name 327 | attr_reader :description 328 | 329 | attr_accessor :default 330 | alias help_default default 331 | 332 | def help_opt 333 | "--#{@name}=#{@template}" 334 | end 335 | 336 | def value? 337 | true 338 | end 339 | 340 | def value 341 | @value 342 | end 343 | 344 | def resolve(table) 345 | @value.gsub(%r<\$([^/]+)>) { table[$1] } 346 | end 347 | 348 | def set(val) 349 | @value = check(val) 350 | end 351 | 352 | private 353 | 354 | def check(val) 355 | setup_rb_error "config: --#{name} requires argument" unless val 356 | val 357 | end 358 | end 359 | 360 | class BoolItem < Item 361 | def config_type 362 | 'bool' 363 | end 364 | 365 | def help_opt 366 | "--#{@name}" 367 | end 368 | 369 | private 370 | 371 | def check(val) 372 | return 'yes' unless val 373 | case val 374 | when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' 375 | when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' 376 | else 377 | setup_rb_error "config: --#{@name} accepts only yes/no for argument" 378 | end 379 | end 380 | end 381 | 382 | class PathItem < Item 383 | def config_type 384 | 'path' 385 | end 386 | 387 | private 388 | 389 | def check(path) 390 | setup_rb_error "config: --#{@name} requires argument" unless path 391 | path[0,1] == '$' ? path : File.expand_path(path) 392 | end 393 | end 394 | 395 | class ProgramItem < Item 396 | def config_type 397 | 'program' 398 | end 399 | end 400 | 401 | class SelectItem < Item 402 | def initialize(name, selection, default, desc) 403 | super 404 | @ok = selection.split('/') 405 | end 406 | 407 | def config_type 408 | 'select' 409 | end 410 | 411 | private 412 | 413 | def check(val) 414 | unless @ok.include?(val.strip) 415 | setup_rb_error "config: use --#{@name}=#{@template} (#{val})" 416 | end 417 | val.strip 418 | end 419 | end 420 | 421 | class ExecItem < Item 422 | def initialize(name, selection, desc, &block) 423 | super name, selection, nil, desc 424 | @ok = selection.split('/') 425 | @action = block 426 | end 427 | 428 | def config_type 429 | 'exec' 430 | end 431 | 432 | def value? 433 | false 434 | end 435 | 436 | def resolve(table) 437 | setup_rb_error "$#{name()} wrongly used as option value" 438 | end 439 | 440 | undef set 441 | 442 | def evaluate(val, table) 443 | v = val.strip.downcase 444 | unless @ok.include?(v) 445 | setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" 446 | end 447 | @action.call v, table 448 | end 449 | end 450 | 451 | class PackageSelectionItem < Item 452 | def initialize(name, template, default, help_default, desc) 453 | super name, template, default, desc 454 | @help_default = help_default 455 | end 456 | 457 | attr_reader :help_default 458 | 459 | def config_type 460 | 'package' 461 | end 462 | 463 | private 464 | 465 | def check(val) 466 | unless File.dir?("packages/#{val}") 467 | setup_rb_error "config: no such package: #{val}" 468 | end 469 | val 470 | end 471 | end 472 | 473 | class MetaConfigEnvironment 474 | def initialize(config, installer) 475 | @config = config 476 | @installer = installer 477 | end 478 | 479 | def config_names 480 | @config.names 481 | end 482 | 483 | def config?(name) 484 | @config.key?(name) 485 | end 486 | 487 | def bool_config?(name) 488 | @config.lookup(name).config_type == 'bool' 489 | end 490 | 491 | def path_config?(name) 492 | @config.lookup(name).config_type == 'path' 493 | end 494 | 495 | def value_config?(name) 496 | @config.lookup(name).config_type != 'exec' 497 | end 498 | 499 | def add_config(item) 500 | @config.add item 501 | end 502 | 503 | def add_bool_config(name, default, desc) 504 | @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) 505 | end 506 | 507 | def add_path_config(name, default, desc) 508 | @config.add PathItem.new(name, 'path', default, desc) 509 | end 510 | 511 | def set_config_default(name, default) 512 | @config.lookup(name).default = default 513 | end 514 | 515 | def remove_config(name) 516 | @config.remove(name) 517 | end 518 | 519 | # For only multipackage 520 | def packages 521 | raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer 522 | @installer.packages 523 | end 524 | 525 | # For only multipackage 526 | def declare_packages(list) 527 | raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer 528 | @installer.packages = list 529 | end 530 | end 531 | 532 | end # class ConfigTable 533 | 534 | 535 | # This module requires: #verbose?, #no_harm? 536 | module FileOperations 537 | 538 | def mkdir_p(dirname, prefix = nil) 539 | dirname = prefix + File.expand_path(dirname) if prefix 540 | $stderr.puts "mkdir -p #{dirname}" if verbose? 541 | return if no_harm? 542 | 543 | # Does not check '/', it's too abnormal. 544 | dirs = File.expand_path(dirname).split(%r<(?=/)>) 545 | if /\A[a-z]:\z/i =~ dirs[0] 546 | disk = dirs.shift 547 | dirs[0] = disk + dirs[0] 548 | end 549 | dirs.each_index do |idx| 550 | path = dirs[0..idx].join('') 551 | Dir.mkdir path unless File.dir?(path) 552 | end 553 | end 554 | 555 | def rm_f(path) 556 | $stderr.puts "rm -f #{path}" if verbose? 557 | return if no_harm? 558 | force_remove_file path 559 | end 560 | 561 | def rm_rf(path) 562 | $stderr.puts "rm -rf #{path}" if verbose? 563 | return if no_harm? 564 | remove_tree path 565 | end 566 | 567 | def remove_tree(path) 568 | if File.symlink?(path) 569 | remove_file path 570 | elsif File.dir?(path) 571 | remove_tree0 path 572 | else 573 | force_remove_file path 574 | end 575 | end 576 | 577 | def remove_tree0(path) 578 | Dir.foreach(path) do |ent| 579 | next if ent == '.' 580 | next if ent == '..' 581 | entpath = "#{path}/#{ent}" 582 | if File.symlink?(entpath) 583 | remove_file entpath 584 | elsif File.dir?(entpath) 585 | remove_tree0 entpath 586 | else 587 | force_remove_file entpath 588 | end 589 | end 590 | begin 591 | Dir.rmdir path 592 | rescue Errno::ENOTEMPTY 593 | # directory may not be empty 594 | end 595 | end 596 | 597 | def move_file(src, dest) 598 | force_remove_file dest 599 | begin 600 | File.rename src, dest 601 | rescue 602 | File.open(dest, 'wb') {|f| 603 | f.write File.binread(src) 604 | } 605 | File.chmod File.stat(src).mode, dest 606 | File.unlink src 607 | end 608 | end 609 | 610 | def force_remove_file(path) 611 | begin 612 | remove_file path 613 | rescue 614 | end 615 | end 616 | 617 | def remove_file(path) 618 | File.chmod 0777, path 619 | File.unlink path 620 | end 621 | 622 | def install(from, dest, mode, prefix = nil) 623 | $stderr.puts "install #{from} #{dest}" if verbose? 624 | return if no_harm? 625 | 626 | realdest = prefix ? prefix + File.expand_path(dest) : dest 627 | realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) 628 | str = File.binread(from) 629 | if diff?(str, realdest) 630 | verbose_off { 631 | rm_f realdest if File.exist?(realdest) 632 | } 633 | File.open(realdest, 'wb') {|f| 634 | f.write str 635 | } 636 | File.chmod mode, realdest 637 | 638 | File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| 639 | if prefix 640 | f.puts realdest.sub(prefix, '') 641 | else 642 | f.puts realdest 643 | end 644 | } 645 | end 646 | end 647 | 648 | def diff?(new_content, path) 649 | return true unless File.exist?(path) 650 | new_content != File.binread(path) 651 | end 652 | 653 | def command(*args) 654 | $stderr.puts args.join(' ') if verbose? 655 | system(*args) or raise RuntimeError, 656 | "system(#{args.map{|a| a.inspect }.join(' ')}) failed" 657 | end 658 | 659 | def ruby(*args) 660 | command config('rubyprog'), *args 661 | end 662 | 663 | def make(task = nil) 664 | command(*[config('makeprog'), task].compact) 665 | end 666 | 667 | def extdir?(dir) 668 | File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") 669 | end 670 | 671 | def files_of(dir) 672 | Dir.open(dir) {|d| 673 | return d.select {|ent| File.file?("#{dir}/#{ent}") } 674 | } 675 | end 676 | 677 | DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) 678 | 679 | def directories_of(dir) 680 | Dir.open(dir) {|d| 681 | return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT 682 | } 683 | end 684 | 685 | end 686 | 687 | 688 | # This module requires: #srcdir_root, #objdir_root, #relpath 689 | module HookScriptAPI 690 | 691 | def get_config(key) 692 | @config[key] 693 | end 694 | 695 | alias config get_config 696 | 697 | # obsolete: use metaconfig to change configuration 698 | def set_config(key, val) 699 | @config[key] = val 700 | end 701 | 702 | # 703 | # srcdir/objdir (works only in the package directory) 704 | # 705 | 706 | def curr_srcdir 707 | "#{srcdir_root()}/#{relpath()}" 708 | end 709 | 710 | def curr_objdir 711 | "#{objdir_root()}/#{relpath()}" 712 | end 713 | 714 | def srcfile(path) 715 | "#{curr_srcdir()}/#{path}" 716 | end 717 | 718 | def srcexist?(path) 719 | File.exist?(srcfile(path)) 720 | end 721 | 722 | def srcdirectory?(path) 723 | File.dir?(srcfile(path)) 724 | end 725 | 726 | def srcfile?(path) 727 | File.file?(srcfile(path)) 728 | end 729 | 730 | def srcentries(path = '.') 731 | Dir.open("#{curr_srcdir()}/#{path}") {|d| 732 | return d.to_a - %w(. ..) 733 | } 734 | end 735 | 736 | def srcfiles(path = '.') 737 | srcentries(path).select {|fname| 738 | File.file?(File.join(curr_srcdir(), path, fname)) 739 | } 740 | end 741 | 742 | def srcdirectories(path = '.') 743 | srcentries(path).select {|fname| 744 | File.dir?(File.join(curr_srcdir(), path, fname)) 745 | } 746 | end 747 | 748 | end 749 | 750 | 751 | class ToplevelInstaller 752 | 753 | Version = '3.4.1' 754 | Copyright = 'Copyright (c) 2000-2005 Minero Aoki' 755 | 756 | TASKS = [ 757 | [ 'all', 'do config, setup, then install' ], 758 | [ 'config', 'saves your configurations' ], 759 | [ 'show', 'shows current configuration' ], 760 | [ 'setup', 'compiles ruby extentions and others' ], 761 | [ 'install', 'installs files' ], 762 | [ 'test', 'run all tests in test/' ], 763 | [ 'clean', "does `make clean' for each extention" ], 764 | [ 'distclean',"does `make distclean' for each extention" ] 765 | ] 766 | 767 | def ToplevelInstaller.invoke 768 | config = ConfigTable.new(load_rbconfig()) 769 | config.load_standard_entries 770 | config.load_multipackage_entries if multipackage? 771 | config.fixup 772 | klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) 773 | klass.new(File.dirname($0), config).invoke 774 | end 775 | 776 | def ToplevelInstaller.multipackage? 777 | File.dir?(File.dirname($0) + '/packages') 778 | end 779 | 780 | def ToplevelInstaller.load_rbconfig 781 | if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } 782 | ARGV.delete(arg) 783 | load File.expand_path(arg.split(/=/, 2)[1]) 784 | $".push 'rbconfig.rb' 785 | else 786 | require 'rbconfig' 787 | end 788 | ::Config::CONFIG 789 | end 790 | 791 | def initialize(ardir_root, config) 792 | @ardir = File.expand_path(ardir_root) 793 | @config = config 794 | # cache 795 | @valid_task_re = nil 796 | end 797 | 798 | def config(key) 799 | @config[key] 800 | end 801 | 802 | def inspect 803 | "#<#{self.class} #{__id__()}>" 804 | end 805 | 806 | def invoke 807 | run_metaconfigs 808 | case task = parsearg_global() 809 | when nil, 'all' 810 | parsearg_config 811 | init_installers 812 | exec_config 813 | exec_setup 814 | exec_install 815 | else 816 | case task 817 | when 'config', 'test' 818 | ; 819 | when 'clean', 'distclean' 820 | @config.load_savefile if File.exist?(@config.savefile) 821 | else 822 | @config.load_savefile 823 | end 824 | __send__ "parsearg_#{task}" 825 | init_installers 826 | __send__ "exec_#{task}" 827 | end 828 | end 829 | 830 | def run_metaconfigs 831 | @config.load_script "#{@ardir}/metaconfig" 832 | end 833 | 834 | def init_installers 835 | @installer = Installer.new(@config, @ardir, File.expand_path('.')) 836 | end 837 | 838 | # 839 | # Hook Script API bases 840 | # 841 | 842 | def srcdir_root 843 | @ardir 844 | end 845 | 846 | def objdir_root 847 | '.' 848 | end 849 | 850 | def relpath 851 | '.' 852 | end 853 | 854 | # 855 | # Option Parsing 856 | # 857 | 858 | def parsearg_global 859 | while arg = ARGV.shift 860 | case arg 861 | when /\A\w+\z/ 862 | setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) 863 | return arg 864 | when '-q', '--quiet' 865 | @config.verbose = false 866 | when '--verbose' 867 | @config.verbose = true 868 | when '--help' 869 | print_usage $stdout 870 | exit 0 871 | when '--version' 872 | puts "#{File.basename($0)} version #{Version}" 873 | exit 0 874 | when '--copyright' 875 | puts Copyright 876 | exit 0 877 | else 878 | setup_rb_error "unknown global option '#{arg}'" 879 | end 880 | end 881 | nil 882 | end 883 | 884 | def valid_task?(t) 885 | valid_task_re() =~ t 886 | end 887 | 888 | def valid_task_re 889 | @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ 890 | end 891 | 892 | def parsearg_no_options 893 | unless ARGV.empty? 894 | task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) 895 | setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" 896 | end 897 | end 898 | 899 | alias parsearg_show parsearg_no_options 900 | alias parsearg_setup parsearg_no_options 901 | alias parsearg_test parsearg_no_options 902 | alias parsearg_clean parsearg_no_options 903 | alias parsearg_distclean parsearg_no_options 904 | 905 | def parsearg_config 906 | evalopt = [] 907 | set = [] 908 | @config.config_opt = [] 909 | while i = ARGV.shift 910 | if /\A--?\z/ =~ i 911 | @config.config_opt = ARGV.dup 912 | break 913 | end 914 | name, value = *@config.parse_opt(i) 915 | if @config.value_config?(name) 916 | @config[name] = value 917 | else 918 | evalopt.push [name, value] 919 | end 920 | set.push name 921 | end 922 | evalopt.each do |name, value| 923 | @config.lookup(name).evaluate value, @config 924 | end 925 | # Check if configuration is valid 926 | set.each do |n| 927 | @config[n] if @config.value_config?(n) 928 | end 929 | end 930 | 931 | def parsearg_install 932 | @config.no_harm = false 933 | @config.install_prefix = '' 934 | while a = ARGV.shift 935 | case a 936 | when '--no-harm' 937 | @config.no_harm = true 938 | when /\A--prefix=/ 939 | path = a.split(/=/, 2)[1] 940 | path = File.expand_path(path) unless path[0,1] == '/' 941 | @config.install_prefix = path 942 | else 943 | setup_rb_error "install: unknown option #{a}" 944 | end 945 | end 946 | end 947 | 948 | def print_usage(out) 949 | out.puts 'Typical Installation Procedure:' 950 | out.puts " $ ruby #{File.basename $0} config" 951 | out.puts " $ ruby #{File.basename $0} setup" 952 | out.puts " # ruby #{File.basename $0} install (may require root privilege)" 953 | out.puts 954 | out.puts 'Detailed Usage:' 955 | out.puts " ruby #{File.basename $0} " 956 | out.puts " ruby #{File.basename $0} [] []" 957 | 958 | fmt = " %-24s %s\n" 959 | out.puts 960 | out.puts 'Global options:' 961 | out.printf fmt, '-q,--quiet', 'suppress message outputs' 962 | out.printf fmt, ' --verbose', 'output messages verbosely' 963 | out.printf fmt, ' --help', 'print this message' 964 | out.printf fmt, ' --version', 'print version and quit' 965 | out.printf fmt, ' --copyright', 'print copyright and quit' 966 | out.puts 967 | out.puts 'Tasks:' 968 | TASKS.each do |name, desc| 969 | out.printf fmt, name, desc 970 | end 971 | 972 | fmt = " %-24s %s [%s]\n" 973 | out.puts 974 | out.puts 'Options for CONFIG or ALL:' 975 | @config.each do |item| 976 | out.printf fmt, item.help_opt, item.description, item.help_default 977 | end 978 | out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" 979 | out.puts 980 | out.puts 'Options for INSTALL:' 981 | out.printf fmt, '--no-harm', 'only display what to do if given', 'off' 982 | out.printf fmt, '--prefix=path', 'install path prefix', '' 983 | out.puts 984 | end 985 | 986 | # 987 | # Task Handlers 988 | # 989 | 990 | def exec_config 991 | @installer.exec_config 992 | @config.save # must be final 993 | end 994 | 995 | def exec_setup 996 | @installer.exec_setup 997 | end 998 | 999 | def exec_install 1000 | @installer.exec_install 1001 | end 1002 | 1003 | def exec_test 1004 | @installer.exec_test 1005 | end 1006 | 1007 | def exec_show 1008 | @config.each do |i| 1009 | printf "%-20s %s\n", i.name, i.value if i.value? 1010 | end 1011 | end 1012 | 1013 | def exec_clean 1014 | @installer.exec_clean 1015 | end 1016 | 1017 | def exec_distclean 1018 | @installer.exec_distclean 1019 | end 1020 | 1021 | end # class ToplevelInstaller 1022 | 1023 | 1024 | class ToplevelInstallerMulti < ToplevelInstaller 1025 | 1026 | include FileOperations 1027 | 1028 | def initialize(ardir_root, config) 1029 | super 1030 | @packages = directories_of("#{@ardir}/packages") 1031 | raise 'no package exists' if @packages.empty? 1032 | @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) 1033 | end 1034 | 1035 | def run_metaconfigs 1036 | @config.load_script "#{@ardir}/metaconfig", self 1037 | @packages.each do |name| 1038 | @config.load_script "#{@ardir}/packages/#{name}/metaconfig" 1039 | end 1040 | end 1041 | 1042 | attr_reader :packages 1043 | 1044 | def packages=(list) 1045 | raise 'package list is empty' if list.empty? 1046 | list.each do |name| 1047 | raise "directory packages/#{name} does not exist"\ 1048 | unless File.dir?("#{@ardir}/packages/#{name}") 1049 | end 1050 | @packages = list 1051 | end 1052 | 1053 | def init_installers 1054 | @installers = {} 1055 | @packages.each do |pack| 1056 | @installers[pack] = Installer.new(@config, 1057 | "#{@ardir}/packages/#{pack}", 1058 | "packages/#{pack}") 1059 | end 1060 | with = extract_selection(config('with')) 1061 | without = extract_selection(config('without')) 1062 | @selected = @installers.keys.select {|name| 1063 | (with.empty? or with.include?(name)) \ 1064 | and not without.include?(name) 1065 | } 1066 | end 1067 | 1068 | def extract_selection(list) 1069 | a = list.split(/,/) 1070 | a.each do |name| 1071 | setup_rb_error "no such package: #{name}" unless @installers.key?(name) 1072 | end 1073 | a 1074 | end 1075 | 1076 | def print_usage(f) 1077 | super 1078 | f.puts 'Inluded packages:' 1079 | f.puts ' ' + @packages.sort.join(' ') 1080 | f.puts 1081 | end 1082 | 1083 | # 1084 | # Task Handlers 1085 | # 1086 | 1087 | def exec_config 1088 | run_hook 'pre-config' 1089 | each_selected_installers {|inst| inst.exec_config } 1090 | run_hook 'post-config' 1091 | @config.save # must be final 1092 | end 1093 | 1094 | def exec_setup 1095 | run_hook 'pre-setup' 1096 | each_selected_installers {|inst| inst.exec_setup } 1097 | run_hook 'post-setup' 1098 | end 1099 | 1100 | def exec_install 1101 | run_hook 'pre-install' 1102 | each_selected_installers {|inst| inst.exec_install } 1103 | run_hook 'post-install' 1104 | end 1105 | 1106 | def exec_test 1107 | run_hook 'pre-test' 1108 | each_selected_installers {|inst| inst.exec_test } 1109 | run_hook 'post-test' 1110 | end 1111 | 1112 | def exec_clean 1113 | rm_f @config.savefile 1114 | run_hook 'pre-clean' 1115 | each_selected_installers {|inst| inst.exec_clean } 1116 | run_hook 'post-clean' 1117 | end 1118 | 1119 | def exec_distclean 1120 | rm_f @config.savefile 1121 | run_hook 'pre-distclean' 1122 | each_selected_installers {|inst| inst.exec_distclean } 1123 | run_hook 'post-distclean' 1124 | end 1125 | 1126 | # 1127 | # lib 1128 | # 1129 | 1130 | def each_selected_installers 1131 | Dir.mkdir 'packages' unless File.dir?('packages') 1132 | @selected.each do |pack| 1133 | $stderr.puts "Processing the package `#{pack}' ..." if verbose? 1134 | Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") 1135 | Dir.chdir "packages/#{pack}" 1136 | yield @installers[pack] 1137 | Dir.chdir '../..' 1138 | end 1139 | end 1140 | 1141 | def run_hook(id) 1142 | @root_installer.run_hook id 1143 | end 1144 | 1145 | # module FileOperations requires this 1146 | def verbose? 1147 | @config.verbose? 1148 | end 1149 | 1150 | # module FileOperations requires this 1151 | def no_harm? 1152 | @config.no_harm? 1153 | end 1154 | 1155 | end # class ToplevelInstallerMulti 1156 | 1157 | 1158 | class Installer 1159 | 1160 | FILETYPES = %w( bin lib ext data conf man ) 1161 | 1162 | include FileOperations 1163 | include HookScriptAPI 1164 | 1165 | def initialize(config, srcroot, objroot) 1166 | @config = config 1167 | @srcdir = File.expand_path(srcroot) 1168 | @objdir = File.expand_path(objroot) 1169 | @currdir = '.' 1170 | end 1171 | 1172 | def inspect 1173 | "#<#{self.class} #{File.basename(@srcdir)}>" 1174 | end 1175 | 1176 | def noop(rel) 1177 | end 1178 | 1179 | # 1180 | # Hook Script API base methods 1181 | # 1182 | 1183 | def srcdir_root 1184 | @srcdir 1185 | end 1186 | 1187 | def objdir_root 1188 | @objdir 1189 | end 1190 | 1191 | def relpath 1192 | @currdir 1193 | end 1194 | 1195 | # 1196 | # Config Access 1197 | # 1198 | 1199 | # module FileOperations requires this 1200 | def verbose? 1201 | @config.verbose? 1202 | end 1203 | 1204 | # module FileOperations requires this 1205 | def no_harm? 1206 | @config.no_harm? 1207 | end 1208 | 1209 | def verbose_off 1210 | begin 1211 | save, @config.verbose = @config.verbose?, false 1212 | yield 1213 | ensure 1214 | @config.verbose = save 1215 | end 1216 | end 1217 | 1218 | # 1219 | # TASK config 1220 | # 1221 | 1222 | def exec_config 1223 | exec_task_traverse 'config' 1224 | end 1225 | 1226 | alias config_dir_bin noop 1227 | alias config_dir_lib noop 1228 | 1229 | def config_dir_ext(rel) 1230 | extconf if extdir?(curr_srcdir()) 1231 | end 1232 | 1233 | alias config_dir_data noop 1234 | alias config_dir_conf noop 1235 | alias config_dir_man noop 1236 | 1237 | def extconf 1238 | ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt 1239 | end 1240 | 1241 | # 1242 | # TASK setup 1243 | # 1244 | 1245 | def exec_setup 1246 | exec_task_traverse 'setup' 1247 | end 1248 | 1249 | def setup_dir_bin(rel) 1250 | files_of(curr_srcdir()).each do |fname| 1251 | update_shebang_line "#{curr_srcdir()}/#{fname}" 1252 | end 1253 | end 1254 | 1255 | alias setup_dir_lib noop 1256 | 1257 | def setup_dir_ext(rel) 1258 | make if extdir?(curr_srcdir()) 1259 | end 1260 | 1261 | alias setup_dir_data noop 1262 | alias setup_dir_conf noop 1263 | alias setup_dir_man noop 1264 | 1265 | def update_shebang_line(path) 1266 | return if no_harm? 1267 | return if config('shebang') == 'never' 1268 | old = Shebang.load(path) 1269 | if old 1270 | $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 1271 | new = new_shebang(old) 1272 | return if new.to_s == old.to_s 1273 | else 1274 | return unless config('shebang') == 'all' 1275 | new = Shebang.new(config('rubypath')) 1276 | end 1277 | $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? 1278 | open_atomic_writer(path) {|output| 1279 | File.open(path, 'rb') {|f| 1280 | f.gets if old # discard 1281 | output.puts new.to_s 1282 | output.print f.read 1283 | } 1284 | } 1285 | end 1286 | 1287 | def new_shebang(old) 1288 | if /\Aruby/ =~ File.basename(old.cmd) 1289 | Shebang.new(config('rubypath'), old.args) 1290 | elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' 1291 | Shebang.new(config('rubypath'), old.args[1..-1]) 1292 | else 1293 | return old unless config('shebang') == 'all' 1294 | Shebang.new(config('rubypath')) 1295 | end 1296 | end 1297 | 1298 | def open_atomic_writer(path, &block) 1299 | tmpfile = File.basename(path) + '.tmp' 1300 | begin 1301 | File.open(tmpfile, 'wb', &block) 1302 | File.rename tmpfile, File.basename(path) 1303 | ensure 1304 | File.unlink tmpfile if File.exist?(tmpfile) 1305 | end 1306 | end 1307 | 1308 | class Shebang 1309 | def Shebang.load(path) 1310 | line = nil 1311 | File.open(path) {|f| 1312 | line = f.gets 1313 | } 1314 | return nil unless /\A#!/ =~ line 1315 | parse(line) 1316 | end 1317 | 1318 | def Shebang.parse(line) 1319 | cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') 1320 | new(cmd, args) 1321 | end 1322 | 1323 | def initialize(cmd, args = []) 1324 | @cmd = cmd 1325 | @args = args 1326 | end 1327 | 1328 | attr_reader :cmd 1329 | attr_reader :args 1330 | 1331 | def to_s 1332 | "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") 1333 | end 1334 | end 1335 | 1336 | # 1337 | # TASK install 1338 | # 1339 | 1340 | def exec_install 1341 | rm_f 'InstalledFiles' 1342 | exec_task_traverse 'install' 1343 | end 1344 | 1345 | def install_dir_bin(rel) 1346 | install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 1347 | end 1348 | 1349 | def install_dir_lib(rel) 1350 | install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 1351 | end 1352 | 1353 | def install_dir_ext(rel) 1354 | return unless extdir?(curr_srcdir()) 1355 | install_files rubyextentions('.'), 1356 | "#{config('sodir')}/#{File.dirname(rel)}", 1357 | 0555 1358 | end 1359 | 1360 | def install_dir_data(rel) 1361 | install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 1362 | end 1363 | 1364 | def install_dir_conf(rel) 1365 | # FIXME: should not remove current config files 1366 | # (rename previous file to .old/.org) 1367 | install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 1368 | end 1369 | 1370 | def install_dir_man(rel) 1371 | install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 1372 | end 1373 | 1374 | def install_files(list, dest, mode) 1375 | mkdir_p dest, @config.install_prefix 1376 | list.each do |fname| 1377 | install fname, dest, mode, @config.install_prefix 1378 | end 1379 | end 1380 | 1381 | def libfiles 1382 | glob_reject(%w(*.y *.output), targetfiles()) 1383 | end 1384 | 1385 | def rubyextentions(dir) 1386 | ents = glob_select("*.#{@config.dllext}", targetfiles()) 1387 | if ents.empty? 1388 | setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" 1389 | end 1390 | ents 1391 | end 1392 | 1393 | def targetfiles 1394 | mapdir(existfiles() - hookfiles()) 1395 | end 1396 | 1397 | def mapdir(ents) 1398 | ents.map {|ent| 1399 | if File.exist?(ent) 1400 | then ent # objdir 1401 | else "#{curr_srcdir()}/#{ent}" # srcdir 1402 | end 1403 | } 1404 | end 1405 | 1406 | # picked up many entries from cvs-1.11.1/src/ignore.c 1407 | JUNK_FILES = %w( 1408 | core RCSLOG tags TAGS .make.state 1409 | .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb 1410 | *~ *.old *.bak *.BAK *.orig *.rej _$* *$ 1411 | 1412 | *.org *.in .* 1413 | ) 1414 | 1415 | def existfiles 1416 | glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) 1417 | end 1418 | 1419 | def hookfiles 1420 | %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| 1421 | %w( config setup install clean ).map {|t| sprintf(fmt, t) } 1422 | }.flatten 1423 | end 1424 | 1425 | def glob_select(pat, ents) 1426 | re = globs2re([pat]) 1427 | ents.select {|ent| re =~ ent } 1428 | end 1429 | 1430 | def glob_reject(pats, ents) 1431 | re = globs2re(pats) 1432 | ents.reject {|ent| re =~ ent } 1433 | end 1434 | 1435 | GLOB2REGEX = { 1436 | '.' => '\.', 1437 | '$' => '\$', 1438 | '#' => '\#', 1439 | '*' => '.*' 1440 | } 1441 | 1442 | def globs2re(pats) 1443 | /\A(?:#{ 1444 | pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') 1445 | })\z/ 1446 | end 1447 | 1448 | # 1449 | # TASK test 1450 | # 1451 | 1452 | TESTDIR = 'test' 1453 | 1454 | def exec_test 1455 | unless File.directory?('test') 1456 | $stderr.puts 'no test in this package' if verbose? 1457 | return 1458 | end 1459 | $stderr.puts 'Running tests...' if verbose? 1460 | begin 1461 | require 'test/unit' 1462 | rescue LoadError 1463 | setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' 1464 | end 1465 | runner = Test::Unit::AutoRunner.new(true) 1466 | runner.to_run << TESTDIR 1467 | runner.run 1468 | end 1469 | 1470 | # 1471 | # TASK clean 1472 | # 1473 | 1474 | def exec_clean 1475 | exec_task_traverse 'clean' 1476 | rm_f @config.savefile 1477 | rm_f 'InstalledFiles' 1478 | end 1479 | 1480 | alias clean_dir_bin noop 1481 | alias clean_dir_lib noop 1482 | alias clean_dir_data noop 1483 | alias clean_dir_conf noop 1484 | alias clean_dir_man noop 1485 | 1486 | def clean_dir_ext(rel) 1487 | return unless extdir?(curr_srcdir()) 1488 | make 'clean' if File.file?('Makefile') 1489 | end 1490 | 1491 | # 1492 | # TASK distclean 1493 | # 1494 | 1495 | def exec_distclean 1496 | exec_task_traverse 'distclean' 1497 | rm_f @config.savefile 1498 | rm_f 'InstalledFiles' 1499 | end 1500 | 1501 | alias distclean_dir_bin noop 1502 | alias distclean_dir_lib noop 1503 | 1504 | def distclean_dir_ext(rel) 1505 | return unless extdir?(curr_srcdir()) 1506 | make 'distclean' if File.file?('Makefile') 1507 | end 1508 | 1509 | alias distclean_dir_data noop 1510 | alias distclean_dir_conf noop 1511 | alias distclean_dir_man noop 1512 | 1513 | # 1514 | # Traversing 1515 | # 1516 | 1517 | def exec_task_traverse(task) 1518 | run_hook "pre-#{task}" 1519 | FILETYPES.each do |type| 1520 | if type == 'ext' and config('without-ext') == 'yes' 1521 | $stderr.puts 'skipping ext/* by user option' if verbose? 1522 | next 1523 | end 1524 | traverse task, type, "#{task}_dir_#{type}" 1525 | end 1526 | run_hook "post-#{task}" 1527 | end 1528 | 1529 | def traverse(task, rel, mid) 1530 | dive_into(rel) { 1531 | run_hook "pre-#{task}" 1532 | __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') 1533 | directories_of(curr_srcdir()).each do |d| 1534 | traverse task, "#{rel}/#{d}", mid 1535 | end 1536 | run_hook "post-#{task}" 1537 | } 1538 | end 1539 | 1540 | def dive_into(rel) 1541 | return unless File.dir?("#{@srcdir}/#{rel}") 1542 | 1543 | dir = File.basename(rel) 1544 | Dir.mkdir dir unless File.dir?(dir) 1545 | prevdir = Dir.pwd 1546 | Dir.chdir dir 1547 | $stderr.puts '---> ' + rel if verbose? 1548 | @currdir = rel 1549 | yield 1550 | Dir.chdir prevdir 1551 | $stderr.puts '<--- ' + rel if verbose? 1552 | @currdir = File.dirname(rel) 1553 | end 1554 | 1555 | def run_hook(id) 1556 | path = [ "#{curr_srcdir()}/#{id}", 1557 | "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } 1558 | return unless path 1559 | begin 1560 | instance_eval File.read(path), path, 1 1561 | rescue 1562 | raise if $DEBUG 1563 | setup_rb_error "hook #{path} failed:\n" + $!.message 1564 | end 1565 | end 1566 | 1567 | end # class Installer 1568 | 1569 | 1570 | class SetupError < StandardError; end 1571 | 1572 | def setup_rb_error(msg) 1573 | raise SetupError, msg 1574 | end 1575 | 1576 | if $0 == __FILE__ 1577 | begin 1578 | ToplevelInstaller.invoke 1579 | rescue SetupError 1580 | raise if $DEBUG 1581 | $stderr.puts $!.message 1582 | $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." 1583 | exit 1 1584 | end 1585 | end 1586 | -------------------------------------------------------------------------------- /static/css/control.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: arial, helvetica, sans-serif; 3 | font-size: 16px; 4 | margin: 20px; 5 | background-color: #f5f8e1; 6 | text-align: center; 7 | } 8 | 9 | h1, h2, h3, h4, h5 { 10 | font-weight: normal; 11 | margin: 0; padding: 0; 12 | } 13 | 14 | h1 { font-size: 15px; color: #797; } 15 | h2 { font-size: 34px; } 16 | h3 { font-size: 26px; margin-top: 12px; } 17 | h4 { font-size: 21px; } 18 | 19 | #page { 20 | width: 640px; 21 | margin: 0 auto; 22 | padding: 3px 20px; 23 | border: solid 1px #d5d8c1; 24 | background: white url(/control/s/images/monopoly-car.jpg) 340px -60px no-repeat; 25 | text-align: left; 26 | } 27 | 28 | #header, #content { 29 | margin: 12px 0px; 30 | } 31 | 32 | .menu { 33 | float: right; 34 | font-size: 11px; 35 | } 36 | 37 | .menu ul { 38 | list-style: none; 39 | margin: 0; padding: 0; 40 | } 41 | 42 | .menu ul li { 43 | display: inline; 44 | margin: 0; padding: 0; 45 | } 46 | 47 | .menu ul li a { 48 | color: black; 49 | text-decoration: none; 50 | margin: 0px 2px; padding: 3px 6px; 51 | font-weight: bold; 52 | background-color: #f1f4d8; 53 | -moz-opacity: 0.92; 54 | } 55 | 56 | .menu ul li a.active { 57 | background-color: #f1e428; 58 | } 59 | 60 | .menu ul li a:hover { 61 | color: white; 62 | background-color: #C13438; 63 | } 64 | 65 | ul.errors { 66 | color: black; 67 | background-color: #f1e428; 68 | padding: 4px; 69 | margin: 0 0 10px 0; 70 | -moz-opacity: 0.80; 71 | width: 250px; 72 | list-style: none; 73 | } 74 | 75 | form.create { 76 | padding: 0 0 10px 0; 77 | } 78 | 79 | form.create .required, 80 | form.create .optional { 81 | padding: 5px 0; 82 | } 83 | 84 | form.create label { 85 | display: block; 86 | color: #575; 87 | font-size: 10px; 88 | } 89 | 90 | form.create div.inline input, 91 | form.create div.inline label, 92 | form.create div.inline select 93 | { 94 | display: inline; 95 | width: auto; 96 | } 97 | 98 | form.create div input, 99 | form.create div select { 100 | font-size: 16px; 101 | width: 250px; 102 | padding: 1px; 103 | } 104 | 105 | form.create div input.fixed { 106 | font-family: "Lucida Console", monospace; 107 | } 108 | 109 | form.create div input.large { 110 | font-size: 21px; 111 | font-weight: bold; 112 | } 113 | 114 | form.create div input.long { 115 | width: 450px; 116 | } 117 | 118 | table { 119 | border-collapse: collapse; 120 | border: 2px solid #3f3c5f; 121 | color: #000; 122 | background: #fff; 123 | width: 100%; 124 | } 125 | caption { 126 | margin: 0.3em 0; 127 | font-size: .7em; 128 | font-weight: normal; 129 | text-align: left; 130 | color: #000; 131 | background: transparent; 132 | } 133 | td, th { 134 | border: 1px solid #336; 135 | padding: 0.3em; 136 | } 137 | thead th { 138 | border: 1px solid #336; 139 | text-align: left; 140 | font-weight: bold; 141 | background-color: #f1f478; 142 | color: #333; 143 | } 144 | tfoot th, tfoot td { 145 | border: 1px solid #396; 146 | text-align: left; 147 | background: #e8e8cf; 148 | } 149 | tfoot th { 150 | font-weight: bold; 151 | } 152 | tbody td a { 153 | background: transparent; 154 | color: #00c; 155 | text-decoration: underline; 156 | } 157 | tbody td a:hover { 158 | background: transparent; 159 | color: #00c; 160 | text-decoration: underline; 161 | } 162 | tbody tr:hover th { 163 | background-color: #c66; 164 | } 165 | tbody tr:hover th a { 166 | color: white; 167 | } 168 | tbody th a { 169 | background: transparent; 170 | color: #3f7c5f; 171 | text-decoration: underline; 172 | font-weight: normal; 173 | } 174 | tbody th, tbody td { 175 | vertical-align: top; 176 | text-align: left; 177 | font-size: 15px; 178 | } 179 | tfoot td { 180 | border: 1px solid #996; 181 | } 182 | tbody tr:hover { 183 | background: #efffd9; 184 | } 185 | tbody th:hover p { 186 | color: #fec; 187 | } 188 | tbody th p { 189 | font-size: 12px; 190 | font-weight: normal; 191 | margin: 4px 0; 192 | } 193 | .details { 194 | display: none; 195 | padding-left: 12px; 196 | } 197 | .details p { 198 | font-size: 10px; 199 | line-height: 110%; 200 | } 201 | -------------------------------------------------------------------------------- /static/images/monopoly-car.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattjamieson/parkplace/1c44cca4bb11acd800043eff913e64bbdce87942/static/images/monopoly-car.jpg -------------------------------------------------------------------------------- /static/js/jquery.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery - Current 3 | * http://jquery.com/ 4 | * 5 | * To use, download this file to your server, save as jquery.js, 6 | * and add this HTML into the ... of your web page: 7 | * 8 | * 9 | * Copyright (c) 2006 John Resig 10 | * Licensed under the MIT License: 11 | * http://www.opensource.org/licenses/mit-license.php 12 | */ 13 | /* Built Fri May 12 13:01:23 2006 */ 14 | eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('7 $(a,c){8 $a=a||$.14||R;8 $c=c&&c.$4k&&c.1l(0)||c;l(1O 4g!="2r"){l($a.N==1g){8 S=I 1i("[^a-41-6z-6y-]");l(!S.3Z($a)){$c=$c&&$c.2L||R;l($c.2t($a).q==0){8 1m=$c.25($a);l(1m!=C)k 1m}}}H l($a.N==36){k $.1w($a,7(b){l(b.N==1g)k R.25(b);k b})}}8 T={B:$.2d($a,$c),$4k:"$6x: 29 $",1E:7(){k 6.1l().q},1l:7(i){k i==C?6.B:6.B[i]},E:7(f){D(8 i=0;i<6.1E();i++)$.1f(6.1l(i),f,[i]);k 6},3e:7(a,b){k 6.E(7(){l(b==C)D(8 j 1d a)$.W(6,j,a[j]);H $.W(6,a,b)})},3f:7(h){k h==C&&6.1E()?6.1l(0).1Z:6.3e("1Z",h)},2J:7(h){k h==C&&6.1E()?6.1l(0).2R:6.3e("2R",h)},1n:7(a,b){k a.N!=1g||b?6.E(7(){l(!b)D(8 j 1d a)$.W(6.L,j,a[j]);H $.W(6.L,a,b)}):$.1n(6.1l(0),a)},2s:7(){k 6.E(7(){8 d=$.19(6,"V");l(d=="1z"||d==\'\')$(6).1v();H $(6).1u()})},1v:7(a){k 6.E(7(){6.L.V=6.$$2k?6.$$2k:\'\';l($.19(6,"V")=="1z")6.L.V=\'33\'})},1u:7(a){k 6.E(7(){6.$$2k=$.19(6,"V");l(6.$$2k=="1z")6.$$2k=\'33\';6.L.V=\'1z\'})},6w:7(c){k 6.E(7(){l($.2b(6,c))k;6.1j+=(6.1j.q>0?" ":"")+c})},6v:7(c){k 6.E(7(){6.1j=c==C?\'\':6.1j.1x(I 1i(\'(^|\\\\s*\\\\b[^-])\'+c+\'($|\\\\b(?=[^-]))\',\'g\'),\'\')})},6u:7(c){k 6.E(7(){l($.2b(6,c))6.1j=6.1j.1x(I 1i(\'(\\\\s*\\\\b[^-])\'+c+\'($|\\\\b(?=[^-]))\',\'g\'),\'\');H 6.1j+=(6.1j.q>0?" ":"")+c})},6t:7(){6.E(7(){6.U.4h(6)});6.B=[];k 6},6s:7(){8 a=$.1X(1M);k 6.E(7(){8 b=a[0].2j(Q);6.U.2M(b,6);1G(b.1W)b=b.1W;b.4j(6)})},4i:7(){8 1D=6.1E()>1;8 a=$.1X(1M);k 6.E(7(){D(8 i=0;i1;8 a=$.1X(1M);k 6.E(7(){D(8 i=a.q-1;i>=0;i--)6.2M(1D?a[i].2j(Q):a[i],6.1W)})},6p:7(){8 1D=6.1E()>1;8 a=$.1X(1M);k 6.E(7(){D(8 i=0;i1;8 a=$.1X(1M);k 6.E(7(){D(8 i=a.q-1;i>=0;i--)6.U.2M(1D?a[i].2j(Q):a[i],6.6n)})},45:7(){k 6.E(7(){1G(6.1W)6.4h(6.1W)})},21:7(t,f){k 6.E(7(){1F(6,t,f)})},3B:7(t,f){k 6.E(7(){35(6,t,f)})},3A:7(t){k 6.E(7(){2U(6,t)})},2Q:7(t){8 1Y=[],F=[];6.E(7(){1Y[1Y.q]=6;F=$.Y(F,$.2d(t,6))});6.1Y=1Y;6.B=F;k 6},6m:7(){6.B=6.1Y;k 6},46:7(a){6.B=$.1w(6.B,7(d){k d.U});l(a)6.B=$.15(a,6.B).r;k 6},38:7(a){6.B=$.1w(6.B,$.38);l(a)6.B=$.15(a,6.B).r;k 6},6l:7(a){6.B=$.1w(6.B,$.12);l(a)6.B=$.15(a,6.B).r;k 6},15:7(t){6.B=$.15(t,6.B).r;k 6},2G:7(t){6.B=t.N==1g?$.15(t,6.B,16).r:$.1S(6.B,7(a){k a!=t});k 6},6k:7(t){6.B=$.Y(6.B,t.N==1g?$.2d(t):t.N==36?t:[t]);k 6},6j:7(t){k $.15(t,6.B).r.q>0},6i:7(t){k!6.s(t)}};D(8 i 1d $.G){l(T[i]!=C)T["1I"+i]=T[i];T[i]=$.G[i]}l(1O 4g!="2r"&&$a.N!=1g){l($c)$a=T.1l();D(8 i 1d T){(7(j){2P{l($a[j]==C){$a[j]=7(){k $.1f(T,T[j],1M)}}}2N(e){}})(i)}k $a}k T}$.1f=7(o,f,a){a=a||[];l(f.1f)k f.1f(o,a);H{8 p=[];D(8 i=0;i m[3]-0",2h:"m[3] - 0 == i",69:"m[3] - 0 == i",3c:"i == 0",1e:"i == r.q - 1",49:"i % 2 == 0",48:"i % 2 == 1","3c-2f":"$.12(a,0).B","2h-2f":"(m[3] == \'49\'?$.12(a,m[3]).n % 2 == 0 :(m[3] == \'48\'?$.12(a,m[3]).n % 2 == 1:$.12(a,m[3]).B))","1e-2f":"$.12(a,0,Q).B","2h-1e-2f":"$.12(a,m[3],Q).B","3c-2g-u":"$.1T(a,0)","2h-2g-u":"$.1T(a,m[3])","1e-2g-u":"$.1T(a,0,Q)","2h-1e-2g-u":"$.1T(a,m[3],Q)","47-2g-u":"$.1T(a) == 1","47-2f":"$.12(a).q == 1",46:"a.1a.q > 0",45:"a.1a.q == 0",68:"a == ( a.44 ? a.44 : R ).2L",67:"(a.66 || a.1Z).O(m[3]) != -1",65:"(!a.u || a.u != \'1q\') && ($.19(a,\'V\') != \'1z\' && $.19(a,\'2e\') != \'1q\')",1q:"(a.u && a.u == \'1q\') || $.19(a,\'V\') == \'1z\' || $.19(a,\'2e\') == \'1q\'",3k:"a.3b == 16",3b:"a.3b",2T:"a.2T"},".":"$.2b(a,m[2])","@":{"=":"$.W(a,m[3]) == m[4]","!=":"$.W(a,m[3]) != m[4]","~=":"$.2b($.W(a,m[3]),m[4])","|=":"$.W(a,m[3]).O(m[4]) == 0","^=":"$.W(a,m[3]).O(m[4]) == 0","$=":"$.W(a,m[3]).1o( $.W(a,m[3]).q - m[4].q, m[4].q ) == m[4]","*=":"$.W(a,m[3]).O(m[4]) >= 0","":"m[3] == \'*\' ? a.64.q > 0 : $.W(a,m[3])"},"[":"$.2d(m[2],a).q > 0"};$.G={};$.2d=7(t,14){14=14||$.14||R;l(t.N!=1g)k[t];l(t.O("//")==0){14=14.2L;t=t.1o(2,t.q)}H l(t.O("/")==0){14=14.2L;t=t.1o(1,t.q);l(t.O(\'/\'))t=t.1o(t.O(\'/\'),t.q)}8 F=[14];8 1V=[];8 1e=C;1G(t.q>0&&1e!=t){8 r=[];1e=t;t=$.1L(t);8 S=I 1i("^//","i");t=t.1x(S,"");l(t.O(\'..\')==0||t.O(\'/..\')==0){l(t.O(\'/\')==0)t=t.1o(1,t.q);r=$.1w(F,7(a){k a.U});t=t.1o(2,t.q);t=$.1L(t)}H l(t.O(\'>\')==0||t.O(\'/\')==0){r=$.1w(F,7(a){k(a.1a.q>0?$.12(a.1W):C)});t=t.1o(1,t.q);t=$.1L(t)}H l(t.O(\'+\')==0){r=$.1w(F,7(a){k $.12(a).40});t=t.1o(1,t.q);t=$.1L(t)}H l(t.O(\'~\')==0){r=$.1w(F,7(a){8 r=[];8 s=$.12(a);l(s.n>0)D(8 i=s.n;i0&&t.5O(/^[:\\\\.#\\\\[a-41-Z\\\\*]/)){8 S=I 1i("^\\\\[ *@([a-2H-9\\\\(\\\\)1I-]+) *([~!\\\\|\\\\*$^=]*) *\'?\\"?([^\'\\"]*)\'?\\"? *\\\\]","i");8 m=S.1C(t);l(m!=C){m=[\'\',\'@\',m[2],m[1],m[3]]}H{8 S=I 1i("^(\\\\[) *([^\\\\]]*) *\\\\]","i");8 m=S.1C(t);l(m==C){8 S=I 1i("^(:)([a-2H-9\\\\*1I-]*)\\\\( *[\\"\']?([^ \\\\)\'\\"]*)[\'\\"]? *\\\\)","i");8 m=S.1C(t);l(m==C){8 S=I 1i("^([:\\\\.#]*)([a-2H-9\\\\*1I-]*)","i");8 m=S.1C(t)}}}t=t.1x(S,"");l(m[1]==":"&&m[2]=="2G")r=$.15(m[3],r,16).r;H{l($.g[m[1]].N==1g)8 f=$.g[m[1]];H l($.g[m[1]][m[2]])8 f=$.g[m[1]][m[2]];l(f!=C){2O("f = 7(a,i){k "+f+"}");r=g(r,f)}}}k{r:r,t:t}};$.38=7(a){8 b=[];8 c=a.U;1G(c!=C&&c!=R){b[b.q]=c;c=c.U}k b};$.1L=7(t){k t.1x(/^\\s+|\\s+$/g,\'\')};$.1T=7(a,n,e){8 t=$.1S($.12(a),7(b){k b.2B==a.2B});l(e)n=t.q-n-1;k n!=C?t[n]==a:t.q};$.12=7(a,n,e){8 u=[];8 2c=a.U.1a;D(8 i=0;i<2c.q;i++){l(2c[i].2C==1)u[u.q]=2c[i];l(2c[i]==a)u.n=u.q-1}l(e)n=u.q-n-1;u.B=(u[n]==a);u.5N=(u.n>0?u[u.n-1]:C);u.40=(u.n0)z.1u();H z.1v()};z.2w=7(a){z.2y(z.B(),z.B()+a)};z.3P=7(){3t(z.1r);z.1r=C};z.M=M.N==1g?R.25(M):M;8 y=z.M.L;z.3N=y.2z;y.2z="1q";z.o={3Q:"32",28:(1A&&1A.28)||2A,1H:(1A&&1A.1H)||1A};z.3I=7(f,2Z){8 t=(I 3K).3J();8 p=(t-z.s)/z.o.28;l(t>=z.o.28+z.s){z.1s=2Z;z.3P();3O(7(){y.2z=z.3N;l(y.18=="3M"||y.1y=="3M")z.27("1z");l(1t!="26"&&z.o.31){$.30(z.M,"18");$.30(z.M,"1y")}l(z.o.1H.N==20){z.M.$1I=z.o.1H;z.M.$1I()}},13)}H z.1s=((-3L.5m(p*3L.5l)/2)+0.5)*(2Z-f)+f;z.a()};z.2y=7(f,t){l(z.1r)k;6.1s=f;z.a();z.2x=z.B();z.s=(I 3K).3J();z.1r=3r(7(){z.3I(f,t)},13)}}J.G=["1v","1u","2s"];J.1t=["3H","3G","5k","5j"];D(8 i 1d J.1t){(7(){8 c=J.1t[i];J[c]=7(a,b){k I J(a,b,c.2W(),c)}})()}J.2u=7(a,b){8 o=I J(a,b,"26");o.B=7(){k 5i(o.M.L.26)};o.a=7(){8 e=o.M.L;l(o.1s==1)o.1s=0.5h;l(23.2X)e.15="5g(26="+o.1s*5f+")";e.26=o.1s};o.2x=o.1s=1;o.a();k o};J.2v=7(e,o){8 z=6;8 h=I J.3H(e,o);l(o)o.1H=C;8 w=I J.3G(e,o);7 c(a,b,c){k(!a||a==c||b==c)}D(8 i 1d J.G){(7(){8 j=J.G[i];z[j]=7(a,b){l(c(a,b,"18"))h[j]();l(c(a,b,"1y"))w[j]()}})()}z.2w=7(c,d){h.2w(c);w.2w(d)}};J.2Y=7(e,o){8 z=6;8 r=I J.2v(e,o);l(o)o.1H=C;8 p=I J.2u(e,o);D(8 i 1d J.G){(7(){8 j=J.G[i];z[j]=7(a,b){p[j]();r[j](a,b)}})()}};8 e=["5e","5d","5c","2n","5b","3F","5a","3q","59","58","57","56","55","54","3z","3x","53","3E","3D","3C","52","51","50","4Z","4Y","17"];D(8 i=0;i=0)?"4L.3p":"4K.3p")}}$.P=7(u,1k,X,F){8 P=I 2V();l(P){P.4J(u||"2m",1k,Q);l(X)P.4I(\'4H-4G\',\'4F/x-4E-4D-4C\');P.4B=7(){l(P.4A==4){l(F)F(P);$.3n($.2q(P))}};P.4z(X)}};$.2q=7(r,u){k r.4y("4x-u").O("P")>0||u=="P"?r.4w:r.3g};$.1l=7(1k,F,u){$.P("2m",1k,C,7(r){l(F)F($.2q(r,u))})};$.4v=7(1k,F){$.1l(1k,F,"P")};$.3o=7(1k,X,F,u){$.P("3h",1k,$.2l(X),7(r){l(F)F($.2q(r,u))})};$.4u=7(1k,X,F){$.3o(1k,X,F,"P")};$.G.4t=7(2o){$.22=$.Y($.22,6.B);k 6.21(\'3m\',2o)};$.22=[];$.3n=7(X){D(8 i=0;i<$.22.q;i++)2U($.22[i],\'3m\',[X])};$.G.4s=7(2o){k 6.E(7(){8 a={};$(6).2Q("2p:2T,1q,1N,4r[@4q],3l").15(":3k").E(7(){a[6.3j||6.2S||6.U.3j||6.U.2S]=6.2R});$.P(6.4p||"2m",6.4o||"",$.2l(a),2o)})};$.2l=7(a){8 s=[];D(8 i 1d a)s[s.q]=i+"="+4n(a[i]);k s.3i("&")};$.G.2n=7(a,o,f){l(a&&a.N==20)k 6.21("2n",a);8 t="2m";l(o&&o.N==20){f=o;o=C}l(o!=C){o=$.2l(o);t="3h"}8 T=6;$.P(t,a,o,7(h){8 h=h.3g;T.3f(h).2Q("4m").E(7(){2P{2O(6.1N||6.4l||6.1Z)}2N(e){}});l(f)f(h)});k 6};',62,408,'||||||this|function|var||||||||||||return|if|||||length||||type|||||||cur|null|for|each|ret|fn|else|new|fx|element|style|el|constructor|indexOf|xml|true|document|re|self|parentNode|display|attr|data|merge|||event|sibling||context|filter|false|ready|height|getCSS|childNodes|events|case|in|last|apply|String|handlers|RegExp|className|url|get|obj|css|substr|els|hidden|timer|now|ty|hide|show|map|replace|width|none|op|handler|exec|clone|size|addEvent|while|onComplete|_|parseInt|speed|cleanSpaces|arguments|text|typeof|position|fixEvent|guid|grep|ofType|tag|done|firstChild|clean|old|innerHTML|Function|bind|ajaxHandles|window|preventDefault|getElementById|opacity|ss|duration||on|hasWord|tmp|Select|visibility|child|of|nth|div|cloneNode|oldblock|param|GET|load|callback|input|httpData|undefined|toggle|getElementsByTagName|Opacity|Resize|modify|io|custom|overflow|400|nodeName|nodeType|stopPropagation|returnValue|handleEvent|not|z0|fix|val|break|documentElement|insertBefore|catch|eval|try|find|value|id|checked|triggerEvent|XMLHttpRequest|toLowerCase|ActiveXObject|FadeSize|tt|setAuto|auto|px|block|tz|removeEvent|Array|getAll|parents|toUpperCase|oid|disabled|first|defaultView|set|html|responseText|POST|join|name|enabled|textarea|ajax|triggerAJAX|post|XMLHTTP|click|setInterval|addEventListener|clearInterval|hover|relatedTarget|toElement|mouseout|fromElement|mouseover|trigger|unbind|submit|select|reset|scroll|Width|Height|step|getTime|Date|Math|0px|oo|setTimeout|clear|unit|max|absolute|center|offsetHeight|offsetWidth|_show|_hide|delete|test|next|zA|setAttribute|shift|ownerDocument|empty|parent|only|odd|even|getComputedStyle|currentStyle|oWidth|oHeight|ov|od|Prototype|removeChild|append|appendChild|jquery|textContent|script|encodeURIComponent|action|method|selected|option|serialize|handleAJAX|postXML|getXML|responseXML|content|getResponseHeader|send|readyState|onreadystatechange|urlencoded|form|www|application|Type|Content|setRequestHeader|open|Msxml2|Microsoft|msie|userAgent|navigator|_toggle|onready|body|push|DOMContentLoaded|onhover|one|do|un|error|abort|keyup|keypress|keydown|change|mousemove|mouseleave|mouseenter|mouseup|mousedown|dblclick|unload|resize|contextmenu|focus|blur|100|alpha|9999|parseFloat|Top|Left|PI|cos|natural|top|left|relative|static|IMG|fadeIn|fadeOut|slideUp|slideDown|number|normal|75|xfast|200|fast|medium|600|slow|850|xslow|1200|crawl|nodeValue|cancelBubble|location|prev|match|getAttribute|ig|cssFloat|float|class|cssText|htmlFor|default|file|password|image|button|checkbox|radio|switch|attributes|visible|innerText|contains|root|eq|gt|lt|toString|createTextNode|createElement|getPropertyValue|clientWidth|clientHeight|isNot|is|add|siblings|end|nextSibling|after|before|prepend|appendTo|wrap|remove|toggleClass|removeClass|addClass|Rev|9_|Z0'.split('|'),0,{})) 15 | --------------------------------------------------------------------------------