SqueezeBox 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +42 -0
- data/Rakefile +104 -0
- data/bin/squeezebox +82 -0
- data/examples/quotes/database.db +0 -0
- data/examples/quotes/database.yml +3 -0
- data/examples/quotes/initialize.rb +14 -0
- data/examples/quotes/layouts/default.erb +13 -0
- data/examples/quotes/public/create.rb +9 -0
- data/examples/quotes/public/delete.rb +5 -0
- data/examples/quotes/public/edit.erb +17 -0
- data/examples/quotes/public/edit.rb +2 -0
- data/examples/quotes/public/exceptions/not_found.erb +5 -0
- data/examples/quotes/public/index.erb +9 -0
- data/examples/quotes/public/index.rb +2 -0
- data/examples/quotes/public/new.erb +14 -0
- data/examples/quotes/public/params.erb +5 -0
- data/examples/quotes/public/site.css +41 -0
- data/examples/quotes/public/update.rb +9 -0
- data/lib/http_error.erb +31 -0
- data/lib/squeeze_box.rb +349 -0
- data/specs/fixtures/enviroment.rb +4 -0
- data/specs/fixtures/public/hello.txt +1 -0
- data/specs/fixtures/public/images/background.jpg +0 -0
- data/specs/fixtures/public/images/background.rb +0 -0
- data/specs/fixtures/public/index.erb +8 -0
- data/specs/fixtures/public/index.rb +6 -0
- data/specs/fixtures/public/internal_server_error.erb +0 -0
- data/specs/fixtures/public/not_found.erb +1 -0
- data/specs/helper.rb +9 -0
- data/specs/http_error_spec.rb +28 -0
- data/specs/resource_spec.rb +33 -0
- metadata +97 -0
data/README
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
SqueezeBox is a Ruby-language web framework for small websites.
|
2
|
+
|
3
|
+
Sometimes Rails is too sophisticated to set up something simple like a poll or quiz for livejournal. Squeeze Box is like 1995; it routes by the file system. Squeeze Box makes large projects painful but small ones simple.
|
4
|
+
|
5
|
+
= Using SqueezeBox
|
6
|
+
Create a directory for your web page. There are two things to put in there
|
7
|
+
* initialize.rb, will be called on to load external libraries, models, or anything else needed to use your site.
|
8
|
+
* public/ the document root directory
|
9
|
+
|
10
|
+
Inside of public/ you place .erb files. The additionally you may place a .rb file of the same basename which contains code that will be called before your erb file loads.
|
11
|
+
|
12
|
+
Example:
|
13
|
+
Local Web
|
14
|
+
/mywebsite/public/happy.erb http://mywebsite.com/happy
|
15
|
+
/mywebsite/public/happy.rb
|
16
|
+
|
17
|
+
/mywebsite/public/index.erb http://mywebsite.com/
|
18
|
+
/mywebsite/public/index.rb
|
19
|
+
|
20
|
+
/mywebsite/public/blah/index.erb http://mywebsite.com/blah
|
21
|
+
/mywebsite/public/blah/index.rb
|
22
|
+
|
23
|
+
The last line of code of the .rb file, if it exists, is tells the SqueezeBox what to do with the request. Typically this command will be 'render' which means 'render the .erb file with the same basename' but it could just be a string of JSON or a redirect.
|
24
|
+
|
25
|
+
= Example Application
|
26
|
+
Please look in examples/quotes for a complete application. To start this run the squeeze_box command with the examples/quotes directory as argument.
|
27
|
+
Something like this:
|
28
|
+
|
29
|
+
lakshmi:~/Projects/squeeze_box% bin/squeeze_box -p 3000 examples/quotes
|
30
|
+
Squeeze Box running at http://0.0.0.0:3000
|
31
|
+
Root: /Users/ry/Projects/squeeze_box/examples/quotes
|
32
|
+
|
33
|
+
|
34
|
+
= Future
|
35
|
+
My plan is to add the basic features necessary for every web application, cookies, sessions, logging. I love haml and will add support for it. Beyond that I want to follow the thin server philosophy and not touch anything else; no plug-in framework, no generators, and certainly no authorization schemes.
|
36
|
+
I don't even want to add ActiveRecord as a dependency.
|
37
|
+
This is to be a spartan and dirty interface to mongrel.
|
38
|
+
The SVN repo is at
|
39
|
+
http://tinyclouds.org/svn/squeeze_box/trunk
|
40
|
+
|
41
|
+
Bug reports, patches, or comments are gladly accepted at
|
42
|
+
ry@tinyclouds.org
|
data/Rakefile
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'spec/rake/spectask'
|
7
|
+
require 'fileutils'
|
8
|
+
def __DIR__
|
9
|
+
File.dirname(__FILE__)
|
10
|
+
end
|
11
|
+
|
12
|
+
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
13
|
+
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = 'SqueezeBox'
|
17
|
+
s.version = "0.0.1"
|
18
|
+
s.summary = "A Web Framework Without Fur or Limestone"
|
19
|
+
s.description = s.summary
|
20
|
+
s.email = 'ry@tinyclouds.org'
|
21
|
+
s.author = 'ry dahl'
|
22
|
+
s.has_rdoc = true
|
23
|
+
s.extra_rdoc_files = %w(README)
|
24
|
+
s.homepage = 'http://squeezebox.rubyforge.org'
|
25
|
+
s.rubyforge_project = 'squeezebox'
|
26
|
+
s.files = FileList['Rakefile', 'lib/**/*', 'bin/*', 'specs/**/*',
|
27
|
+
'examples/**/*']
|
28
|
+
s.executables << 'squeezebox'
|
29
|
+
s.test_files = Dir['specs/**/*_spec.rb']
|
30
|
+
|
31
|
+
s.add_dependency 'mongrel'
|
32
|
+
s.rdoc_options = ['--title', "SqueezeBox",
|
33
|
+
'--main', 'README',
|
34
|
+
'--line-numbers', '--inline-source']
|
35
|
+
end
|
36
|
+
|
37
|
+
Rake::RDocTask.new do |rdoc|
|
38
|
+
files =['README', 'TODO', 'lib/**/*.rb']
|
39
|
+
rdoc.rdoc_files.add(files)
|
40
|
+
rdoc.main = "README" # page to start on
|
41
|
+
rdoc.title = "SqueezeBox"
|
42
|
+
rdoc.rdoc_dir = 'doc' # rdoc output folder
|
43
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
44
|
+
end
|
45
|
+
|
46
|
+
Rake::GemPackageTask.new(spec) do |p|
|
47
|
+
p.need_tar = true
|
48
|
+
p.gem_spec = spec
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "Run specs"
|
52
|
+
Spec::Rake::SpecTask.new('specs') do |t|
|
53
|
+
t.spec_opts = ["--format", "specdoc"]
|
54
|
+
t.spec_files = Dir['specs/*_spec.rb'].sort
|
55
|
+
end
|
56
|
+
|
57
|
+
desc "RCov"
|
58
|
+
Spec::Rake::SpecTask.new('rcov') do |t|
|
59
|
+
t.spec_opts = ["--format", "specdoc"]
|
60
|
+
t.spec_files = Dir['specs/*_spec.rb'].sort
|
61
|
+
t.libs = ['lib' ]
|
62
|
+
t.rcov = true
|
63
|
+
end
|
64
|
+
|
65
|
+
task :deploy => :rdoc do
|
66
|
+
sh %(scp -r website/*.{html,css,png} rydahl@rubyforge.org:/var/www/gforge-projects/squeezebox/)
|
67
|
+
sh %(scp -r doc/ rydahl@rubyforge.org:/var/www/gforge-projects/squeezebox/rdoc/)
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'Tag release'
|
71
|
+
task :tag do
|
72
|
+
svn_root = 'http://ry@tinyclouds.org/svn/squeeze_box'
|
73
|
+
sh %(svn cp #{svn_root}/trunk #{svn_root}/tags/rel-#{spec.version} -m "Tag #{spec.name} release #{spec.version}")
|
74
|
+
end
|
75
|
+
|
76
|
+
task :confirm_release do
|
77
|
+
print "Releasing version #{spec.version}. Are you sure you want to proceed? [Yn] "
|
78
|
+
abort if STDIN.getc == ?n
|
79
|
+
end
|
80
|
+
|
81
|
+
package_name = lambda {|specification| File.join('pkg', "#{specification.name}-#{specification.version}")}
|
82
|
+
|
83
|
+
|
84
|
+
desc 'Push a release to rubyforge'
|
85
|
+
task :release => [:confirm_release, :clean, :package] do
|
86
|
+
require 'rubyforge'
|
87
|
+
package = package_name[spec]
|
88
|
+
|
89
|
+
rubyforge = RubyForge.new
|
90
|
+
rubyforge.login
|
91
|
+
|
92
|
+
# version_already_released = lambda do
|
93
|
+
# releases = rubyforge.userconfig['rubyforge']['release_ids']
|
94
|
+
# releases.has_key?(spec.name) && releases[spec.name][spec.version]
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# abort("Release #{spec.version} already exists!") if version_already_released.call
|
98
|
+
#
|
99
|
+
if release_id = rubyforge.add_release(spec.rubyforge_project, spec.name, spec.version, "#{package}.tar.gz")
|
100
|
+
rubyforge.add_file(spec.rubyforge_project, spec.name, release_id, "#{package}.gem")
|
101
|
+
else
|
102
|
+
puts 'Release failed!'
|
103
|
+
end
|
104
|
+
end
|
data/bin/squeezebox
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'rubygems'
|
5
|
+
|
6
|
+
# this is so dumb that the following doesn't work
|
7
|
+
#autoload(:ActiveRecord, 'active_record')
|
8
|
+
#autoload(:Debugger, 'ruby-debug')
|
9
|
+
require 'active_record'
|
10
|
+
require 'ruby-debug'
|
11
|
+
require File.dirname(__FILE__) + '/../lib/squeeze_box'
|
12
|
+
|
13
|
+
include SqueezeBox
|
14
|
+
|
15
|
+
opts = OptionParser.new do |x|
|
16
|
+
x.banner = "Usage: squeeze_box [pde] path/to/website"
|
17
|
+
|
18
|
+
x.on('-p', '--port PORT') do |p|
|
19
|
+
OPTIONS.port = p.to_i
|
20
|
+
end
|
21
|
+
|
22
|
+
x.on('-d', '--debug') do
|
23
|
+
Debugger.start
|
24
|
+
$mongrel_debug_client = true
|
25
|
+
OPTIONS.debug = true
|
26
|
+
LOGGER.level = Logger::DEBUG
|
27
|
+
end
|
28
|
+
|
29
|
+
environments = %w{development test production}
|
30
|
+
x.on('-e', '--environment ENVIRONMENT', environments.join(' | ')) do |e|
|
31
|
+
if environments.include?(e.downcase!)
|
32
|
+
OPTIONS.environment = e
|
33
|
+
else
|
34
|
+
$stderr.puts "Invalid environment: #{e}"
|
35
|
+
exit 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
x.on("-h", "--help") do
|
40
|
+
puts x
|
41
|
+
exit
|
42
|
+
end
|
43
|
+
end
|
44
|
+
opts.parse!(ARGV)
|
45
|
+
|
46
|
+
unless(ARGV.length == 1)
|
47
|
+
puts opts
|
48
|
+
exit 1
|
49
|
+
end
|
50
|
+
|
51
|
+
OPTIONS.root = File.expand_path ARGV.shift
|
52
|
+
|
53
|
+
# Add the root dir to include path
|
54
|
+
$: << OPTIONS.root
|
55
|
+
|
56
|
+
# Change working dir
|
57
|
+
Dir.chdir( OPTIONS.root )
|
58
|
+
|
59
|
+
|
60
|
+
# Load Initialize Script
|
61
|
+
init_file = File.join(OPTIONS.root, 'initialize.rb')
|
62
|
+
require init_file if File.exists?(init_file)
|
63
|
+
|
64
|
+
# Load database.yml
|
65
|
+
db_config_file = File.join(OPTIONS.root, 'database.yml')
|
66
|
+
if File.exists?(db_config_file)
|
67
|
+
db_config = YAML::load(File.open(db_config_file))[OPTIONS.environment || 'development']
|
68
|
+
ActiveRecord::Base.establish_connection(db_config)
|
69
|
+
end
|
70
|
+
|
71
|
+
server = Mongrel::HttpServer.new('0.0.0.0', OPTIONS.port)
|
72
|
+
server.register("/", SqueezeBox::Handler.new)
|
73
|
+
|
74
|
+
LOGGER.info "Squeeze Box running at http://0.0.0.0:#{OPTIONS.port}"
|
75
|
+
LOGGER.info "Root: #{OPTIONS.root}"
|
76
|
+
trap("INT") do
|
77
|
+
LOGGER.info "Shutting down"
|
78
|
+
server.graceful_shutdown
|
79
|
+
exit
|
80
|
+
end
|
81
|
+
server.run.join
|
82
|
+
|
Binary file
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
class Quote < ActiveRecord::Base
|
3
|
+
end
|
4
|
+
|
5
|
+
|
6
|
+
set_layouts(
|
7
|
+
:default => 'layouts/default.erb'
|
8
|
+
)
|
9
|
+
|
10
|
+
def find_quote(quote_id)
|
11
|
+
quote = Quote.find(:first, :conditions => { :id => quote_id })
|
12
|
+
raise(NotFound, "Cannot find quote with id = #{quote_id}") if quote.nil?
|
13
|
+
quote
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<title>Quotes <%= ' | ' + @page_title if @page_title %></title>
|
4
|
+
<link type="text/css" rel="stylesheet" href="/site.css" media="screen"/>
|
5
|
+
</head>
|
6
|
+
<body>
|
7
|
+
<h1 id="pageTitle"><%= @page_title ? @page_title : 'Quotes' %></h1>
|
8
|
+
<div id="mainContent">
|
9
|
+
<%= content %>
|
10
|
+
</div>
|
11
|
+
<div id="footer">:D</div>
|
12
|
+
</body>
|
13
|
+
</html>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<% @page_title = 'Edit Quote' %>
|
2
|
+
<form action="/update?id=<%= @quote.id %>" method="post">
|
3
|
+
<p>
|
4
|
+
<label for="author">Author</label>
|
5
|
+
<input type="text" name="quote[author]" id="author" value="<%= @quote.author %>"/>
|
6
|
+
</p>
|
7
|
+
|
8
|
+
<p>
|
9
|
+
<label for="content">Content</label>
|
10
|
+
<textarea name="quote[content]" id="content" rows="5"><%= @quote.content %></textarea>
|
11
|
+
</p>
|
12
|
+
|
13
|
+
<input type="submit" value="Update"/>
|
14
|
+
</form>
|
15
|
+
<form action="/delete?id=<%= @quote.id %>" method="post">
|
16
|
+
<input type="submit" value="Delete"/>
|
17
|
+
</form>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<% @page_title = 'All Quotes' %>
|
2
|
+
<a href="/new" class="action">new</a>
|
3
|
+
<% @quotes.each_with_index do |quote, i| %>
|
4
|
+
<div class='quote <%= i%2==0 ? 'even' : 'odd' %>'>
|
5
|
+
<h2 class="author"><%= quote.author %></h2>
|
6
|
+
<div class="content"><%= quote.content %></div>
|
7
|
+
<a href="/edit?id=<%= quote.id %>">edit</a>
|
8
|
+
</div>
|
9
|
+
<% end %>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<% @page_title = 'New Quote' %>
|
2
|
+
<form action="/create" method="post">
|
3
|
+
<p>
|
4
|
+
<label for="author">Author</label>
|
5
|
+
<input type="text" name="quote[author]" id="author" />
|
6
|
+
</p>
|
7
|
+
|
8
|
+
<p>
|
9
|
+
<label for="content">Content</label>
|
10
|
+
<textarea name="quote[content]" id="content" rows="5"></textarea>
|
11
|
+
</p>
|
12
|
+
|
13
|
+
<input type="submit" value="Create"/>
|
14
|
+
</form>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
body {
|
2
|
+
font-family: Arial;
|
3
|
+
margin: 0;
|
4
|
+
}
|
5
|
+
|
6
|
+
h1#pageTitle {
|
7
|
+
text-align: center; padding: 1em;
|
8
|
+
background: #ffa; border-bottom: 1px solid #999;
|
9
|
+
}
|
10
|
+
|
11
|
+
label { display: block; }
|
12
|
+
|
13
|
+
#mainContent {
|
14
|
+
margin: 1em;
|
15
|
+
}
|
16
|
+
|
17
|
+
#footer {
|
18
|
+
margin: 3em 0;
|
19
|
+
border-top: 1px dotted #aaa;
|
20
|
+
font-family: Courier;
|
21
|
+
text-align: center;
|
22
|
+
font-size: 20pt;
|
23
|
+
}
|
24
|
+
|
25
|
+
.action { display: block; margin: 1em; text-align: center;}
|
26
|
+
|
27
|
+
.quote {
|
28
|
+
padding: 5em;
|
29
|
+
}
|
30
|
+
|
31
|
+
.author {
|
32
|
+
font-size: inherit;
|
33
|
+
margin: 0;
|
34
|
+
}
|
35
|
+
|
36
|
+
.content {
|
37
|
+
margin: 0;
|
38
|
+
}
|
39
|
+
|
40
|
+
.even { background: #eef; }
|
41
|
+
.odd { }
|
data/lib/http_error.erb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
4
|
+
<head>
|
5
|
+
<style>
|
6
|
+
body { margin: 1em; font-family: Courier; color: #444;}
|
7
|
+
#httpStatus { text-align: center; margin: 0.2em; }
|
8
|
+
#httpStatus h1, #httpStatus h2 {
|
9
|
+
padding: 0.0em; line-height: 90%; margin: 0;
|
10
|
+
}
|
11
|
+
#httpStatus h1 { font-size: 130pt; }
|
12
|
+
#httpStatus h2 { font-size: 20pt; }
|
13
|
+
li { margin: 1em 0;}
|
14
|
+
</style>
|
15
|
+
<title><%= @status %> <%= @name %></title>
|
16
|
+
</head>
|
17
|
+
<body>
|
18
|
+
<div id="httpStatus">
|
19
|
+
<h1><%= @status %></h1>
|
20
|
+
<h2><%= @message %></h2>
|
21
|
+
</div>
|
22
|
+
<% if @backtrace and SqueezeBox::OPTIONS.environment == 'development' %>
|
23
|
+
<h2>Backtrace</h2>
|
24
|
+
<ol>
|
25
|
+
<% @backtrace.each do |level| %>
|
26
|
+
<li><%= level %></li>
|
27
|
+
<% end %>
|
28
|
+
</ol>
|
29
|
+
<% end %>
|
30
|
+
</body>
|
31
|
+
<html>
|
data/lib/squeeze_box.rb
ADDED
@@ -0,0 +1,349 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'mongrel'
|
3
|
+
require 'erubis'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module SqueezeBox
|
7
|
+
LOGGER = Logger.new(STDOUT)
|
8
|
+
OPTIONS = OpenStruct.new(
|
9
|
+
:debug => false,
|
10
|
+
:port => 4200,
|
11
|
+
:environment => 'development'
|
12
|
+
)
|
13
|
+
module Const
|
14
|
+
include Mongrel::Const
|
15
|
+
MIME_TYPES = Mongrel::DirHandler::MIME_TYPES
|
16
|
+
EXCEPTION_TEMPLATE = Erubis::Eruby.new(File.read(
|
17
|
+
File.join(File.dirname(__FILE__), 'http_error.erb') # lib/http_error.erb
|
18
|
+
))
|
19
|
+
HTTP_COOKIE = 'HTTP_COOKIE'.freeze
|
20
|
+
QUERY_STRING = 'QUERY_STRING'.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
module HTTPErrorMixin
|
24
|
+
def resource_path
|
25
|
+
"/exceptions/#{self.class.name.split('::').last.snake_case}"
|
26
|
+
end
|
27
|
+
def status; self.class::STATUS; end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Unauthorized < StandardError
|
31
|
+
include HTTPErrorMixin
|
32
|
+
STATUS = 401
|
33
|
+
end
|
34
|
+
class Forbidden < StandardError
|
35
|
+
include HTTPErrorMixin
|
36
|
+
STATUS = 403
|
37
|
+
end
|
38
|
+
class NotFound < StandardError
|
39
|
+
include HTTPErrorMixin
|
40
|
+
STATUS = 404
|
41
|
+
end
|
42
|
+
class MethodNotAllowed < StandardError
|
43
|
+
include HTTPErrorMixin
|
44
|
+
STATUS = 405
|
45
|
+
end
|
46
|
+
|
47
|
+
# Alias this method from Resource for easy access
|
48
|
+
def set_layouts(layouts); Resource.set_layouts(layouts); end
|
49
|
+
|
50
|
+
class Resource
|
51
|
+
@@cache = {} # {normalized_path => resource}
|
52
|
+
@@layouts = {} # { symbol => Erubis template}. :default layout is default
|
53
|
+
|
54
|
+
def self.root
|
55
|
+
File.expand_path(File.join(OPTIONS.root, 'public'))
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.set_layouts(layouts)
|
59
|
+
layouts.each do |sym, filename|
|
60
|
+
path = File.join(OPTIONS.root, filename)
|
61
|
+
@@layouts[sym] = Erubis::Eruby.new(File.read(path))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Finds a cached Resource in the cache
|
66
|
+
def self.find(path)
|
67
|
+
path = normalize_path(path)
|
68
|
+
if @@cache.has_key?(path)
|
69
|
+
resource = @@cache[path]
|
70
|
+
resource.reload unless OPTIONS.environment == 'production'
|
71
|
+
resource
|
72
|
+
else # the resource doesn't exist yet
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.clear_cache; @@cache = {}; end
|
78
|
+
|
79
|
+
# Takes a path relative to Resource.root or a full filesystem path
|
80
|
+
# and returns a normalized path.
|
81
|
+
def self.normalize_path(path)
|
82
|
+
path = File.expand_path(path)
|
83
|
+
full_path = path.index(root) == 0 ? path : File.join(root, path)
|
84
|
+
full_path = File.join(full_path, 'index') if File.directory?(full_path)
|
85
|
+
File.extensionless_path(full_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
attr_reader :path
|
89
|
+
|
90
|
+
# =Options=
|
91
|
+
# path: full path to the resource. this path should not have
|
92
|
+
# any extensions e.g. /home/ry/document_root/images/background
|
93
|
+
# not /home/ry/document_root/images/background.jpg
|
94
|
+
# default_template: a default template to use for rendering in the absence
|
95
|
+
# of a resource in the file system (compiled Erubis tempalte)
|
96
|
+
def initialize(path, opts={})
|
97
|
+
LOGGER.info "Resource initialized for #{path}"
|
98
|
+
@path = self.class.normalize_path(path)
|
99
|
+
|
100
|
+
@default_template = opts[:default_template] if opts.has_key? :default_template
|
101
|
+
|
102
|
+
@default_content_type = "application/octet-stream".freeze
|
103
|
+
reload()
|
104
|
+
|
105
|
+
@@cache[@path] = self
|
106
|
+
end
|
107
|
+
|
108
|
+
# reload the .rb and .erb files
|
109
|
+
def reload
|
110
|
+
@static_files = []
|
111
|
+
Dir.glob(@path + '*').each do |file|
|
112
|
+
case File.extname(file)
|
113
|
+
when '.rb'; @logic_file = file
|
114
|
+
when '.erb'; @template_file = file
|
115
|
+
else; @static_files.push(file)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if @logic_file
|
120
|
+
@to_eval = File.read(@logic_file) # !!!
|
121
|
+
|
122
|
+
# Another option is to try to load this data into a method. If we have
|
123
|
+
# users put 'def serve; code; end' in the sever_file, then we could
|
124
|
+
# do something like this:
|
125
|
+
# instance_eval { load(@logic_file)
|
126
|
+
end
|
127
|
+
|
128
|
+
@template =
|
129
|
+
if @template_file
|
130
|
+
Erubis::Eruby.new(File.read(@template_file))
|
131
|
+
else
|
132
|
+
@default_template
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Call this to get the response of an HTTP request. The parameter is a
|
137
|
+
# Mongrel::HttpRequest object
|
138
|
+
def execute(request)
|
139
|
+
@cookies = request.cookies
|
140
|
+
@params = request.cgi_params
|
141
|
+
|
142
|
+
# TODO clean up (is this really nessessary?)
|
143
|
+
path_info = Mongrel::HttpRequest.unescape(request.params[Const::PATH_INFO])
|
144
|
+
@request_extension = File.extname(path_info)
|
145
|
+
|
146
|
+
@header = { Const::CONTENT_TYPE => 'text/html'}
|
147
|
+
@request = request
|
148
|
+
|
149
|
+
# before_filter?
|
150
|
+
# Default is to just render
|
151
|
+
body = @to_eval ? eval(@to_eval) : render()
|
152
|
+
# after_filter?
|
153
|
+
|
154
|
+
rtn = [@status || 200, @header, body]
|
155
|
+
# <TODO> clean this up...
|
156
|
+
# (evidence that a new class to handle requests is needed)
|
157
|
+
# clear variables:
|
158
|
+
@status = @header = @request_extension = @request = @params = @cookies = nil
|
159
|
+
# </TODO>
|
160
|
+
rtn
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def redirect_to(path)
|
166
|
+
@status = 302
|
167
|
+
@header['Location'] = path
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
171
|
+
def render(options = {})
|
172
|
+
# if request is actual file (in @static_files) then serve it
|
173
|
+
if file = @static_files.detect { |f| File.extname(f) == @request_extension }
|
174
|
+
# <TODO> serve with X-SendFile
|
175
|
+
file_status = File.stat(file)
|
176
|
+
@status = 200
|
177
|
+
|
178
|
+
content_type =
|
179
|
+
if dot_at = file.rindex('.')
|
180
|
+
Const::MIME_TYPES[file[dot_at .. -1]] || @default_content_type
|
181
|
+
else
|
182
|
+
@default_content_type
|
183
|
+
end
|
184
|
+
|
185
|
+
etag = Const::ETAG_FORMAT % [file_status.mtime.to_i, file_status.size, file_status.ino]
|
186
|
+
|
187
|
+
@header.update(
|
188
|
+
Const::LAST_MODIFIED => file_status.mtime.httpdate,
|
189
|
+
Const::ETAG => etag,
|
190
|
+
Const::CONTENT_LENGTH => file_status.size,
|
191
|
+
Const::CONTENT_TYPE => content_type
|
192
|
+
)
|
193
|
+
body = File.new(file, 'r')
|
194
|
+
# </TODO>
|
195
|
+
|
196
|
+
# otherwise render the template
|
197
|
+
elsif @template
|
198
|
+
@header[Const::CONTENT_TYPE] = 'text/html'
|
199
|
+
content = @template.result(binding)
|
200
|
+
|
201
|
+
# TODO clean up
|
202
|
+
body =
|
203
|
+
if options[:layout]
|
204
|
+
@@layouts[options[:layout]].result(binding)
|
205
|
+
elsif @@layouts.has_key?(:default) and options[:layout] != false
|
206
|
+
@@layouts[:default].result(binding)
|
207
|
+
else
|
208
|
+
content
|
209
|
+
end
|
210
|
+
|
211
|
+
# otherwise raise 404.
|
212
|
+
else
|
213
|
+
raise NotFound
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
class ExceptionResource < Resource
|
219
|
+
def initialize(path)
|
220
|
+
@default_template = Const::EXCEPTION_TEMPLATE
|
221
|
+
super(path)
|
222
|
+
end
|
223
|
+
|
224
|
+
def set_exception(e)
|
225
|
+
@status = e.respond_to?(:status) ? e.status : 500
|
226
|
+
@message = e.message
|
227
|
+
@backtrace = e.backtrace
|
228
|
+
@name = e.class.name
|
229
|
+
end
|
230
|
+
|
231
|
+
def render(options = {})
|
232
|
+
unless options.has_key?(:layout)
|
233
|
+
options[:layout] = false
|
234
|
+
end
|
235
|
+
super(options)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
class Handler < Mongrel::HttpHandler
|
240
|
+
def process(request, response)
|
241
|
+
path_info = Mongrel::HttpRequest.unescape(request.params[Const::PATH_INFO])
|
242
|
+
LOGGER.info "request: #{path_info}"
|
243
|
+
begin
|
244
|
+
resource = Resource.find(path_info) || Resource.new(path_info)
|
245
|
+
serve_resource(resource, request, response)
|
246
|
+
rescue StandardError => e
|
247
|
+
LOGGER.debug "Exception #{e} raised"
|
248
|
+
resource = Resource.find(e.resource_path) || ExceptionResource.new(e.resource_path)
|
249
|
+
resource.set_exception(e)
|
250
|
+
serve_resource(resource, request, response)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def serve_resource(resource, request, response)
|
255
|
+
response.status, resource_headers, body = resource.execute(request)
|
256
|
+
|
257
|
+
resource_headers[Const::CONTENT_LENGTH] = body.size if body.respond_to?(:size)
|
258
|
+
response.squeeze_box_send_status
|
259
|
+
# Send headers TODO clean up headers
|
260
|
+
resource_headers.each_pair { |k,v| response.header[k] = v }
|
261
|
+
response.send_header
|
262
|
+
|
263
|
+
if resource_headers.has_key?('X-Sendfile')
|
264
|
+
# we're done
|
265
|
+
@body_sent = true
|
266
|
+
elsif body.respond_to? :read
|
267
|
+
while chunk = body.read(Const::CHUNK_SIZE) and chunk.length > 0
|
268
|
+
begin
|
269
|
+
response.write(chunk)
|
270
|
+
rescue Object => exc
|
271
|
+
break
|
272
|
+
end
|
273
|
+
end
|
274
|
+
body.close if body.respond_to? :close
|
275
|
+
# response.write(Const::LINE_END)
|
276
|
+
@body_sent = true
|
277
|
+
else
|
278
|
+
response.write(body || ' ')
|
279
|
+
@body_sent = true
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
|
286
|
+
### Extensions
|
287
|
+
|
288
|
+
class StandardError
|
289
|
+
def resource_path; "/exceptions/index"; end
|
290
|
+
end
|
291
|
+
|
292
|
+
module Mongrel
|
293
|
+
class HttpRequest
|
294
|
+
# parses a query string or the payload of a POST
|
295
|
+
# request into the params hash. So for example:
|
296
|
+
# /foo?bar=nik&post[title]=heya&post[body]=whatever
|
297
|
+
# parses into:
|
298
|
+
# {:bar => 'nik', :post => {:title => 'heya', :body => 'whatever'}}
|
299
|
+
def self.query_parse(qs, d = '&;')
|
300
|
+
m = proc {|_,o,n|o.update(n,&m)rescue([*o]<<n)}
|
301
|
+
(qs||'').split(/[#{d}] */n).inject(Hash[]) do |h,p|
|
302
|
+
k, v=unescape(p).split('=',2)
|
303
|
+
h.update(k.split(/[\]\[]+/).reverse.inject(v) { |x,i| Hash[i,x] },&m)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
# ^--- by Ezra Zygmuntowicz; from merb/mixins/controller.rb
|
307
|
+
|
308
|
+
def post?; params['REQUEST_METHOD'].upcase == 'POST'.upcase; end
|
309
|
+
|
310
|
+
def get?; params['REQUEST_METHOD'].upcase == 'GET'.upcase; end
|
311
|
+
|
312
|
+
def cgi_params
|
313
|
+
unless @_cgi_params
|
314
|
+
@_cgi_params = HttpRequest.query_parse(@params[SqueezeBox::Const::QUERY_STRING] || '')
|
315
|
+
@_cgi_params.update( HttpRequest.query_parse(body.read) ) if post?
|
316
|
+
end
|
317
|
+
@_cgi_params
|
318
|
+
end
|
319
|
+
|
320
|
+
def cookies
|
321
|
+
@_cookies ||= HttpRequest.query_parse(@params[SqueezeBox::Const::HTTP_COOKIE], ';,')
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
class HttpResponse
|
326
|
+
def squeeze_box_send_status
|
327
|
+
if not @status_sent
|
328
|
+
# Do not write Content-Length to header (Bad for streaming)
|
329
|
+
#@header['Content-Length'] = content_length unless @status == 304
|
330
|
+
write(Const::STATUS_FORMAT % [@status, HTTP_STATUS_CODES[@status]])
|
331
|
+
@status_sent = true
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
class File
|
338
|
+
def self.extensionless_path(path)
|
339
|
+
dirname = File.dirname(path)
|
340
|
+
extensionless_basename = File.basename(path).sub(/\.[^.]+$/,'')
|
341
|
+
File.join(dirname, extensionless_basename)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
class String
|
346
|
+
def snake_case
|
347
|
+
gsub(/\B[A-Z]/, '_\&').downcase
|
348
|
+
end
|
349
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Hello World!
|
Binary file
|
File without changes
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
Resource Not Found
|
data/specs/helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helper'
|
2
|
+
|
3
|
+
describe HTTPError do
|
4
|
+
it "should initialize" do
|
5
|
+
x = Unauthorized.new
|
6
|
+
x.class.should == Unauthorized
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should build_resource" do
|
10
|
+
x = NotFound.new
|
11
|
+
resource = x.build_resource
|
12
|
+
resource.class.should == Resource
|
13
|
+
resource.path.should == File.join(SQUEEZE_BOX_ROOT,'public/http_errors/not_found')
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have default template which works" do
|
17
|
+
@backtrace = ['a', 'b', 'c']
|
18
|
+
@status = 404
|
19
|
+
@message = 'Not Found'
|
20
|
+
lambda do
|
21
|
+
out = HTTPError::DEFAULT_TEMPLATE.result(binding)
|
22
|
+
out.should =~ /html/i
|
23
|
+
out.should =~ /Not Found/
|
24
|
+
out.should =~ /404/
|
25
|
+
end.should_not raise_error
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helper'
|
2
|
+
|
3
|
+
describe Resource do
|
4
|
+
it "should instantiate with relative path" do
|
5
|
+
resource = Resource.new('/some_path')
|
6
|
+
resource.class.should == Resource
|
7
|
+
resource.path.should == File.join(SQUEEZE_BOX_ROOT, 'public/some_path')
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should instantiate with full path" do
|
11
|
+
path = File.join(SQUEEZE_BOX_ROOT, 'public/some_path')
|
12
|
+
resource = Resource.new(path)
|
13
|
+
resource.class.should == Resource
|
14
|
+
resource.path.should == path
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should get cached by relative path" do
|
18
|
+
Resource.find('/some_path').should be_nil
|
19
|
+
resource = Resource.new('/some_path')
|
20
|
+
Resource.find('/some_path').should == resource
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should get cached by full path" do
|
24
|
+
path = File.join(SQUEEZE_BOX_ROOT, 'public/some_path')
|
25
|
+
Resource.find(path).should be_nil
|
26
|
+
resource = Resource.new('/some_path')
|
27
|
+
Resource.find(path).should == resource
|
28
|
+
end
|
29
|
+
|
30
|
+
after(:each) do
|
31
|
+
Resource.clear_cache
|
32
|
+
end
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.2
|
3
|
+
specification_version: 1
|
4
|
+
name: SqueezeBox
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2007-08-16 00:00:00 +02:00
|
8
|
+
summary: A Web Framework Without Fur or Limestone
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: ry@tinyclouds.org
|
12
|
+
homepage: http://squeezebox.rubyforge.org
|
13
|
+
rubyforge_project: squeezebox
|
14
|
+
description: A Web Framework Without Fur or Limestone
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- ry dahl
|
31
|
+
files:
|
32
|
+
- Rakefile
|
33
|
+
- lib/http_error.erb
|
34
|
+
- lib/squeeze_box.rb
|
35
|
+
- bin/squeezebox
|
36
|
+
- specs/fixtures
|
37
|
+
- specs/fixtures/enviroment.rb
|
38
|
+
- specs/fixtures/public
|
39
|
+
- specs/fixtures/public/hello.txt
|
40
|
+
- specs/fixtures/public/images
|
41
|
+
- specs/fixtures/public/images/background.jpg
|
42
|
+
- specs/fixtures/public/images/background.rb
|
43
|
+
- specs/fixtures/public/index.erb
|
44
|
+
- specs/fixtures/public/index.rb
|
45
|
+
- specs/fixtures/public/internal_server_error.erb
|
46
|
+
- specs/fixtures/public/not_found.erb
|
47
|
+
- specs/helper.rb
|
48
|
+
- specs/http_error_spec.rb
|
49
|
+
- specs/resource_spec.rb
|
50
|
+
- examples/quotes
|
51
|
+
- examples/quotes/database.db
|
52
|
+
- examples/quotes/database.yml
|
53
|
+
- examples/quotes/initialize.rb
|
54
|
+
- examples/quotes/layouts
|
55
|
+
- examples/quotes/layouts/default.erb
|
56
|
+
- examples/quotes/public
|
57
|
+
- examples/quotes/public/create.rb
|
58
|
+
- examples/quotes/public/delete.rb
|
59
|
+
- examples/quotes/public/edit.erb
|
60
|
+
- examples/quotes/public/edit.rb
|
61
|
+
- examples/quotes/public/exceptions
|
62
|
+
- examples/quotes/public/exceptions/not_found.erb
|
63
|
+
- examples/quotes/public/index.erb
|
64
|
+
- examples/quotes/public/index.rb
|
65
|
+
- examples/quotes/public/new.erb
|
66
|
+
- examples/quotes/public/params.erb
|
67
|
+
- examples/quotes/public/site.css
|
68
|
+
- examples/quotes/public/update.rb
|
69
|
+
- README
|
70
|
+
test_files:
|
71
|
+
- specs/http_error_spec.rb
|
72
|
+
- specs/resource_spec.rb
|
73
|
+
rdoc_options:
|
74
|
+
- --title
|
75
|
+
- SqueezeBox
|
76
|
+
- --main
|
77
|
+
- README
|
78
|
+
- --line-numbers
|
79
|
+
- --inline-source
|
80
|
+
extra_rdoc_files:
|
81
|
+
- README
|
82
|
+
executables:
|
83
|
+
- squeezebox
|
84
|
+
extensions: []
|
85
|
+
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
dependencies:
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: mongrel
|
91
|
+
version_requirement:
|
92
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.0.0
|
97
|
+
version:
|