├── 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 |
--------------------------------------------------------------------------------