gopher 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'gopher'
6
+
7
+ Gopher.application do
8
+ host '0.0.0.0'
9
+ port 70
10
+
11
+ mount '/', '.'
12
+ end
13
+
14
+ Gopher.run
@@ -0,0 +1,14 @@
1
+ class Module
2
+ def dsl_accessor(*accessors)
3
+ accessors.each do |accessor|
4
+ class_eval %{
5
+ attr_writer :#{accessor}
6
+
7
+ def #{accessor}(value=nil)
8
+ send "#{accessor}=", value if value
9
+ @#{accessor}
10
+ end
11
+ }
12
+ end
13
+ end
14
+ end
@@ -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
@@ -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,4 @@
1
+ <!DOCTYPE html>
2
+ <title>gopher.rb redirect</title>
3
+ <meta http-equiv="refresh" content="2;URL=<%= url %>">
4
+ <p>Redirecting to external url <%= url %></p>
@@ -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
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), '/spec_helper')
@@ -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
@@ -0,0 +1,2 @@
1
+ require File.join(File.dirname(__FILE__), '/../lib/gopher')
2
+ require 'spec'
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
+