gopher 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/gopher +14 -0
- data/lib/ext/module.rb +14 -0
- data/lib/ext/object.rb +17 -0
- data/lib/gopher.rb +298 -0
- data/lib/gopher/redirect.erb +4 -0
- data/lib/gopher/redirect.rb +25 -0
- data/spec/application_spec.rb +42 -0
- data/spec/ext_spec.rb +1 -0
- data/spec/gopher_spec.rb +1 -0
- data/spec/handler_spec.rb +45 -0
- data/spec/sandbox/indexed/cake.txt +0 -0
- data/spec/sandbox/recipe.txt +0 -0
- data/spec/spec_helper.rb +2 -0
- metadata +69 -0
data/bin/gopher
ADDED
data/lib/ext/module.rb
ADDED
data/lib/ext/object.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# instance_exec for 1.8
|
2
|
+
# Grabbed from Mauricio Fernandez (thanks!)
|
3
|
+
# For more information, see http://eigenclass.org/hiki.rb?instance_exec
|
4
|
+
class Object
|
5
|
+
module InstanceExecHelper; end
|
6
|
+
include InstanceExecHelper
|
7
|
+
def instance_exec(*args, &block) # !> method redefined; discarding old instance_exec
|
8
|
+
mname = "__instance_exec_#{Thread.current.object_id.abs}_#{object_id.abs}"
|
9
|
+
InstanceExecHelper.module_eval{ define_method(mname, &block) }
|
10
|
+
begin
|
11
|
+
ret = send(mname, *args)
|
12
|
+
ensure
|
13
|
+
InstanceExecHelper.module_eval{ undef_method(mname) } rescue nil
|
14
|
+
end
|
15
|
+
ret
|
16
|
+
end
|
17
|
+
end
|
data/lib/gopher.rb
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'ext/object'
|
4
|
+
require 'ext/module'
|
5
|
+
|
6
|
+
require 'textwrap'
|
7
|
+
require 'yaml'
|
8
|
+
require 'eventmachine'
|
9
|
+
|
10
|
+
module Gopher
|
11
|
+
VERSION = '0.5.1'
|
12
|
+
|
13
|
+
class Application
|
14
|
+
dsl_accessor :host, :port
|
15
|
+
attr_accessor :selectors, :root
|
16
|
+
|
17
|
+
def initialize(&block)
|
18
|
+
reset!
|
19
|
+
self.instance_eval(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def reload(*f); Gopher.reload(*f) end
|
23
|
+
|
24
|
+
def use(mod)
|
25
|
+
extend(mod)
|
26
|
+
end
|
27
|
+
|
28
|
+
def mount(selector, path)
|
29
|
+
add_handler "#{selector}/?(.*)", DirectoryHandler.new(path, selector).with(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
def map(selector, &block)
|
33
|
+
add_handler selector.gsub(/:(.+)/, '(.+)'), MapHandler.new(&block).with(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def text(selector, &block)
|
37
|
+
add_handler selector.gsub(/:(.+)/, '(.+)'), TextHandler.new(&block).with(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
def helpers(&block)
|
41
|
+
MapContext.class_eval(&block)
|
42
|
+
TextContext.class_eval(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def application(selector, handler)
|
46
|
+
add_handler selector, handler
|
47
|
+
end
|
48
|
+
|
49
|
+
def request(selector)
|
50
|
+
handler, *args = lookup(selector)
|
51
|
+
handler.call(*args)
|
52
|
+
end
|
53
|
+
|
54
|
+
def lookup(selector)
|
55
|
+
selectors.find do |k, v|
|
56
|
+
return v, *$~[1..-1] if k =~ Gopher.sanitize_selector(selector)
|
57
|
+
end
|
58
|
+
raise NotFound
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_handler(selector, handler)
|
62
|
+
selector = Gopher.sanitize_selector(selector)
|
63
|
+
selector.sub!(/^\/*/, '')
|
64
|
+
selectors[/^\/?#{selector}$/] = handler
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def reset!
|
69
|
+
@selectors = {}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Handler
|
74
|
+
attr_accessor :application
|
75
|
+
|
76
|
+
def with(application)
|
77
|
+
@application = application
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def host; application.respond_to?(:host) ? application.host : '0.0.0.0' end
|
82
|
+
def port; application.respond_to?(:port) ? application.port : 70 end
|
83
|
+
|
84
|
+
def call(*args); end
|
85
|
+
end
|
86
|
+
|
87
|
+
class MapHandler < Handler
|
88
|
+
attr_accessor :block, :result
|
89
|
+
|
90
|
+
def initialize(&block)
|
91
|
+
@block = block
|
92
|
+
end
|
93
|
+
|
94
|
+
def call(*args)
|
95
|
+
context = MapContext.new(host, port)
|
96
|
+
context.instance_exec(*args, &block)
|
97
|
+
context.result
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class TextHandler < Handler
|
102
|
+
attr_accessor :block, :result
|
103
|
+
|
104
|
+
def initialize(&block)
|
105
|
+
@block = block
|
106
|
+
end
|
107
|
+
|
108
|
+
def call(*args)
|
109
|
+
context = TextContext.new
|
110
|
+
context.instance_exec(*args, &block)
|
111
|
+
context.result
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class DirectoryHandler < Handler
|
116
|
+
attr_accessor :base, :selector, :index
|
117
|
+
|
118
|
+
def initialize(path, selector = '')
|
119
|
+
raise DirectoryNotFound unless File.directory? path
|
120
|
+
@selector = selector
|
121
|
+
@base = File.expand_path(path)
|
122
|
+
@index = YAML.load_file(index_file) if File.exist?(index_file)
|
123
|
+
end
|
124
|
+
|
125
|
+
def call(*args)
|
126
|
+
path = File.join(base, *args)
|
127
|
+
if File.directory? path
|
128
|
+
return DirectoryHandler.new(path, File.join(selector, args)).with(application).to_map
|
129
|
+
elsif File.file? path
|
130
|
+
return File.open(path)
|
131
|
+
else
|
132
|
+
raise NotFound
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def to_map
|
137
|
+
MapContext.with_block(host, port, self) do |handler|
|
138
|
+
if handler.index
|
139
|
+
paragraph handler.index['description']
|
140
|
+
handler.index['entries'].each do |txt, path|
|
141
|
+
link txt, File.join(handler.selector, path)
|
142
|
+
end
|
143
|
+
else
|
144
|
+
Dir["#{handler.base}/*"].each do |path|
|
145
|
+
basename = File.basename(path)
|
146
|
+
if File.directory? path
|
147
|
+
map basename, File.join(handler.selector, basename)
|
148
|
+
else
|
149
|
+
link basename, File.join(handler.selector, basename)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
text Time.now
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
def index_file
|
159
|
+
File.join(base, '.gopher')
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class TextContext
|
164
|
+
attr_accessor :result
|
165
|
+
|
166
|
+
def line(text)
|
167
|
+
result << text
|
168
|
+
result << "\r\n"
|
169
|
+
end
|
170
|
+
|
171
|
+
def text(text)
|
172
|
+
line text
|
173
|
+
end
|
174
|
+
|
175
|
+
def paragraph(txt, width=70)
|
176
|
+
txt.each_line do |line|
|
177
|
+
Textwrap.wrap(line, width).each { |chunk| text chunk }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def initialize
|
182
|
+
@result = ""
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
class MapContext
|
187
|
+
attr_accessor :host, :port, :result
|
188
|
+
|
189
|
+
def initialize(host, port)
|
190
|
+
@host, @port = host, port
|
191
|
+
@result = ""
|
192
|
+
end
|
193
|
+
|
194
|
+
def self.with_block(host, port, *args, &block)
|
195
|
+
new(host, port).instance_exec(*args, &block)
|
196
|
+
end
|
197
|
+
|
198
|
+
def line(type, txt, selector, host = host, port = port)
|
199
|
+
result << ["#{type}#{txt}", selector, host, port].join("\t")
|
200
|
+
result << "\r\n"
|
201
|
+
end
|
202
|
+
|
203
|
+
def link(text, selector, *args)
|
204
|
+
type = Gopher.determine_type(selector)
|
205
|
+
line type, text, scrub(selector), *args
|
206
|
+
end
|
207
|
+
|
208
|
+
def map(text, selector, *args)
|
209
|
+
line '1', text, scrub(selector), *args
|
210
|
+
end
|
211
|
+
|
212
|
+
def text(text)
|
213
|
+
line 'i', text, 'false', '(NULL)', 0
|
214
|
+
end
|
215
|
+
|
216
|
+
def paragraph(txt, width=70)
|
217
|
+
txt.each_line do |line|
|
218
|
+
Textwrap.wrap(line, width).each { |chunk| text chunk }
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
def scrub(s)
|
224
|
+
"/#{s.sub(/^\/+/,'')}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
class GopherError < StandardError; end
|
229
|
+
class DirectoryNotFound < GopherError; end
|
230
|
+
class NotFound < GopherError; end
|
231
|
+
class InvalidRequest < GopherError; end
|
232
|
+
|
233
|
+
def self.sanitize_selector(selector)
|
234
|
+
selector = selector.dup
|
235
|
+
selector.strip!
|
236
|
+
selector.gsub!(/\.+/, '.')
|
237
|
+
selector
|
238
|
+
end
|
239
|
+
def self.determine_type(selector)
|
240
|
+
case File.extname(selector).downcase
|
241
|
+
when '.jpg', '.png' then 'I'
|
242
|
+
when '.mp3' then 's'
|
243
|
+
else '0'
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
class Connection < EM::Connection
|
248
|
+
attr_accessor :application
|
249
|
+
|
250
|
+
def receive_data(data)
|
251
|
+
begin
|
252
|
+
raise InvalidRequest if data.length > 255
|
253
|
+
response = application.request(data)
|
254
|
+
case response
|
255
|
+
when String then send_data(response)
|
256
|
+
when StringIO then send_data(response.read)
|
257
|
+
when File
|
258
|
+
while chunk = response.read(8192) do
|
259
|
+
send_data(chunk)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
close_connection_after_writing
|
263
|
+
rescue Gopher::NotFound
|
264
|
+
send_data "not found"
|
265
|
+
close_connection_after_writing
|
266
|
+
rescue => e
|
267
|
+
close_connection
|
268
|
+
raise e
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def self.application(&block)
|
274
|
+
@application = Application.new(&block)
|
275
|
+
end
|
276
|
+
|
277
|
+
def self.reload(*files)
|
278
|
+
@last_reload = Time.now
|
279
|
+
@reloadables = files
|
280
|
+
end
|
281
|
+
|
282
|
+
def self.reloadables
|
283
|
+
@reloadables ||= []
|
284
|
+
end
|
285
|
+
|
286
|
+
def self.run
|
287
|
+
return if EM.reactor_running?
|
288
|
+
EM.run do
|
289
|
+
EM.start_server(@application.host, @application.port, Gopher::Connection) do |c|
|
290
|
+
c.application = @application
|
291
|
+
reloadables.each do |f|
|
292
|
+
load f if File.mtime(f) > @last_reload
|
293
|
+
end
|
294
|
+
@last_reload = Time.now
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'erubis'
|
2
|
+
|
3
|
+
module Gopher
|
4
|
+
module Redirect
|
5
|
+
def self.extended(application)
|
6
|
+
application.add_handler "URL:(.+)", RedirectHandler.new.with(application)
|
7
|
+
application.class.dsl_accessor :redirect_template
|
8
|
+
application.redirect_template File.join(File.dirname(__FILE__), 'redirect.erb')
|
9
|
+
|
10
|
+
application.helpers do
|
11
|
+
def url(text, url, *args)
|
12
|
+
line 'h', text, "/URL:#{url}", *args
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class RedirectHandler < Handler
|
18
|
+
def call(*url)
|
19
|
+
input = File.read(application.redirect_template)
|
20
|
+
eruby = Erubis::Eruby.new(input)
|
21
|
+
return eruby.result(binding())
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '/spec_helper')
|
2
|
+
|
3
|
+
describe Gopher::Application do
|
4
|
+
before(:all) do
|
5
|
+
@app = Gopher::Application.new do
|
6
|
+
host 'pha.hk'
|
7
|
+
port 7000
|
8
|
+
|
9
|
+
helpers do
|
10
|
+
def ruler
|
11
|
+
text ("=-" * 40)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
mount 'documents', File.dirname(__FILE__)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should set the host' do
|
20
|
+
@app.host.should == 'pha.hk'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should set the port' do
|
24
|
+
@app.port.should == 7000
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should mount directories' do
|
28
|
+
@app.lookup('/documents').should be_instance_of(Array)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should route properly to directories and their children' do
|
32
|
+
@app.lookup('/documents/tasty').should include("tasty")
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should sanitize lookups' do
|
36
|
+
@app.lookup('/documents/.././tasty...html').should include("././tasty.html")
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should add helpers' do
|
40
|
+
Gopher::MapContext.public_instance_methods.should include('ruler')
|
41
|
+
end
|
42
|
+
end
|
data/spec/ext_spec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '/spec_helper')
|
data/spec/gopher_spec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '/spec_helper')
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '/spec_helper')
|
2
|
+
|
3
|
+
describe Gopher::DirectoryHandler do
|
4
|
+
before(:all) do
|
5
|
+
path = File.join(File.dirname(__FILE__), 'sandbox')
|
6
|
+
@handler = Gopher::DirectoryHandler.new(path)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should barf on non-existent directories' do
|
10
|
+
proc { Gopher::DirectoryHandler.new(__FILE__) }.should raise_error(Gopher::DirectoryNotFound)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should serve files under the directory' do
|
14
|
+
@handler.call('recipe.txt').should be_instance_of(File)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should raise a NotFound when files do not exist' do
|
18
|
+
proc { @handler.call('recipe.tx') }.should raise_error(Gopher::NotFound)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should serve generated directory maps' do
|
22
|
+
@handler.call('indexed').should include("An indexed directory")
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should serve directory indexes when applicable' do
|
26
|
+
@handler.call.should include("0recipe.txt\t/recipe.txt\t0.0.0.0\t70\r\n")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe Gopher::MapHandler do
|
31
|
+
before(:all) do
|
32
|
+
@handler = Gopher::MapHandler.new do
|
33
|
+
text 'Cake recipes'
|
34
|
+
link 'Image', 'cake.jpg'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should add text' do
|
39
|
+
@handler.call.should include "iCake recipes\tfalse\t(NULL)\t0\r\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should add links and figure out the types' do
|
43
|
+
@handler.call.should include "IImage\t/cake.jpg\t0.0.0.0\t70\r\n"
|
44
|
+
end
|
45
|
+
end
|
File without changes
|
File without changes
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gopher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- raspberry lemon
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-05-22 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: lemon@raspberry-style.net
|
18
|
+
executables:
|
19
|
+
- gopher
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- bin/gopher
|
26
|
+
- lib/ext
|
27
|
+
- lib/ext/module.rb
|
28
|
+
- lib/ext/object.rb
|
29
|
+
- lib/gopher
|
30
|
+
- lib/gopher/redirect.erb
|
31
|
+
- lib/gopher/redirect.rb
|
32
|
+
- lib/gopher.rb
|
33
|
+
- spec/application_spec.rb
|
34
|
+
- spec/ext_spec.rb
|
35
|
+
- spec/gopher_spec.rb
|
36
|
+
- spec/handler_spec.rb
|
37
|
+
- spec/sandbox
|
38
|
+
- spec/sandbox/indexed
|
39
|
+
- spec/sandbox/indexed/cake.txt
|
40
|
+
- spec/sandbox/recipe.txt
|
41
|
+
- spec/spec_helper.rb
|
42
|
+
has_rdoc: false
|
43
|
+
homepage: http://raspberry-style.net/
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.1.1
|
65
|
+
signing_key:
|
66
|
+
specification_version: 2
|
67
|
+
summary: gopher server and dsl
|
68
|
+
test_files: []
|
69
|
+
|