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.
@@ -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
+