tap-server 0.2.0 → 0.3.0

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/History CHANGED
@@ -1,3 +1,14 @@
1
+ == 0.3.0 / 2009-03-17
2
+
3
+ Significant updates.
4
+
5
+ * added optional REST routes to Tap::Controller
6
+ * added persistence wrapper for session root
7
+ * significant cleanup
8
+ * removed middleware support
9
+ * added bindings for running server from
10
+ Firefox/Ubiquity
11
+
1
12
  == 0.2.0 / 2009-03-07
2
13
 
3
14
  * Reworked server routing to route by ::controller
data/cmd/server.rb CHANGED
@@ -7,13 +7,7 @@ require 'tap/server'
7
7
 
8
8
  env = Tap::Env.instance
9
9
  app = Tap::App.instance
10
-
11
- #
12
- # handle options
13
- #
14
-
15
- config_path = nil
16
- opts = ConfigParser.new do |opts|
10
+ parser = ConfigParser.new do |opts|
17
11
 
18
12
  opts.separator ""
19
13
  opts.separator "options:"
@@ -26,9 +20,7 @@ opts = ConfigParser.new do |opts|
26
20
  exit
27
21
  end
28
22
  end
23
+ argv = parser.parse(ARGV)
29
24
 
30
- # parse!
31
- argv = opts.parse(ARGV)
32
- server = Tap::Server.new(env, app, opts.config)
33
- cookie_server = Rack::Session::Pool.new(server)
34
- Rack::Handler::WEBrick.run(cookie_server, :Port => server.port)
25
+ # launch server
26
+ Tap::Server.new(env, app, parser.config).run!
@@ -1,4 +1,5 @@
1
1
  require 'tap/server'
2
+ require 'tap/support/persistence'
2
3
  autoload(:ERB, 'erb')
3
4
 
4
5
  module Tap
@@ -16,13 +17,62 @@ module Tap
16
17
  # * define it private or protected then call public(:method)
17
18
  #
18
19
  class Controller
20
+
21
+ # Adds REST routing (a-la Rails[http://www.b-simple.de/download/restful_rails_en.pdf])
22
+ # to a Tap::Controller.
23
+ #
24
+ # class Projects < Tap::Controller
25
+ # include RestRoutes
26
+ #
27
+ # # GET /projects
28
+ # def index...
29
+ #
30
+ # # GET /projects/*args
31
+ # def show(*args)...
32
+ #
33
+ # # GET /projects/arg;edit/*args
34
+ # def edit(arg, *args)...
35
+ #
36
+ # # POST /projects/*args
37
+ # def create(*args)...
38
+ #
39
+ # # PUT /projects/*args
40
+ # def update(*args)...
41
+ #
42
+ # # DELETE /projects/*args
43
+ # def destroy(*args)...
44
+ # end
45
+ #
46
+ module RestRoutes
47
+ def route
48
+ blank, *args = request.path_info.split("/").collect {|arg| unescape(arg) }
49
+ action = case request.request_method
50
+ when /GET/i
51
+ case
52
+ when args.empty?
53
+ :index
54
+ when args[0] =~ /(.*);edit$/
55
+ args[0] = $1
56
+ :edit
57
+ else
58
+ :show
59
+ end
60
+ when /POST/i then :create
61
+ when /PUT/i then :update
62
+ when /DELETE/i then :destroy
63
+ else raise ServerError.new("unknown request method: #{request.request_method}")
64
+ end
65
+
66
+ [action, args]
67
+ end
68
+ end
69
+
19
70
  class << self
20
71
 
21
72
  # Initialize instance variables on the child and inherit as necessary.
22
73
  def inherited(child) # :nodoc:
23
74
  super
24
75
  child.set(:actions, actions.dup)
25
- child.set(:middleware, middleware.dup)
26
76
  child.set(:default_layout, default_layout)
27
77
  child.set(:define_action, true)
28
78
  end
@@ -31,10 +81,6 @@ module Tap
31
81
  # stored as symbols. Actions are inherited.
32
82
  attr_reader :actions
33
83
 
34
- # An array of Rack middleware that will be applied when handing requests
35
- # through the class call method. Middleware is inherited.
36
- attr_reader :middleware
37
-
38
84
  # The default layout rendered when the render option :layout is true.
39
85
  attr_reader :default_layout
40
86
 
@@ -44,30 +90,9 @@ module Tap
44
90
  @name ||= to_s.underscore
45
91
  end
46
92
 
47
- # Adds the specified middleware. Middleware classes are initialized
48
- # with the specified args and block, and applied to in the order in
49
- # which they are declared (ie first use processes requests first).
50
- #
51
- # Middleware is applied through the class call method, and on a per-call
52
- # basis... middleware like Rack::Session::Pool that is supposed to
53
- # persist for the life of an application will not work properly.
54
- #
55
- # Middleware is inherited.
56
- def use(middleware, *args, &block)
57
- @middleware << [middleware, args, block]
58
- end
59
-
60
- # Instantiates self and performs call. Middleware is applied in the
61
- # order in which it was declared.
62
- #--
63
- # Note that middleware needs to be initialized in reverese, so that
64
- # the first declared middleware runs first.
93
+ # Instantiates self and performs call.
65
94
  def call(env)
66
- app = new
67
- middleware.reverse_each do |(m, args, block)|
68
- app = m.new(app, *args, &block)
69
- end
70
- app.call(env)
95
+ new.call(env)
71
96
  end
72
97
 
73
98
  # Sets an instance variable for self, short for:
@@ -113,8 +138,10 @@ module Tap
113
138
  end
114
139
 
115
140
  set :actions, []
116
- set :middleware, []
117
141
  set :default_layout, nil
142
+
143
+ # Ensures methods (even public methods) on Controller will
144
+ # not be actions in subclasses.
118
145
  set :define_action, false
119
146
 
120
147
  include Rack::Utils
@@ -130,12 +157,16 @@ module Tap
130
157
  # the action result and response is ignored.
131
158
  attr_accessor :response
132
159
 
160
+ # The action currently being called by self.
161
+ attr_accessor :action
162
+
133
163
  # Initializes a new instance of self. The input attributes are reset by
134
164
  # call and are only provided for convenience during testing.
135
165
  def initialize(server=nil, request=nil, response=nil)
136
166
  @server = server
137
167
  @request = request
138
168
  @response = response
169
+ @action = nil
139
170
  end
140
171
 
141
172
  def call(env)
@@ -144,14 +175,12 @@ module Tap
144
175
  @response = Rack::Response.new
145
176
 
146
177
  # route to an action
147
- blank, action, *args = request.path_info.split("/").collect {|arg| unescape(arg) }
148
- action = "index" if action == nil || action.empty?
149
-
150
- unless self.class.actions.include?(action.to_sym)
178
+ @action, args = route
179
+ unless self.class.actions.include?(@action)
151
180
  raise ServerError.new("404 Error: page not found", 404)
152
181
  end
153
182
 
154
- result = send(action, *args)
183
+ result = send(@action, *args)
155
184
  if result.kind_of?(String)
156
185
  response.write result
157
186
  response.finish
@@ -160,15 +189,23 @@ module Tap
160
189
  end
161
190
  end
162
191
 
192
+ def route
193
+ blank, action, *args = request.path_info.split("/").collect {|arg| unescape(arg) }
194
+ action = "index" if action == nil || action.empty?
195
+ action = action.chomp(File.extname(action)).to_sym
196
+
197
+ [action, args]
198
+ end
199
+
163
200
  def render(path, options={})
164
201
  options, path = path, nil if path.kind_of?(Hash)
165
202
 
166
203
  # lookup template
167
204
  template_path = case
168
205
  when options.has_key?(:template)
169
- server.template_path(options[:template])
206
+ server.search(:views, options[:template])
170
207
  else
171
- server.template_path("#{self.class.name}/#{path}")
208
+ server.search(:views, "#{self.class.name}/#{path}")
172
209
  end
173
210
 
174
211
  unless template_path
@@ -176,7 +213,7 @@ module Tap
176
213
  end
177
214
 
178
215
  # render template
179
- template = server.content(template_path)
216
+ template = File.read(template_path)
180
217
  content = render_erb(template, options)
181
218
 
182
219
  # render layout
@@ -228,6 +265,16 @@ module Tap
228
265
  server.root(session[:id] ||= server.initialize_session)
229
266
  end
230
267
 
268
+ # Returns the file-based controller persistence.
269
+ def persistence
270
+ @persistence ||= Support::Persistence.new(root)
271
+ end
272
+
273
+ # Returns a controller uri.
274
+ def uri(action=nil, params={})
275
+ server.uri(self.class.name, action, params)
276
+ end
277
+
231
278
  # Generates an empty binding to self without any locals assigned.
232
279
  def empty_binding # :nodoc:
233
280
  binding
@@ -13,7 +13,7 @@ module Tap
13
13
  # serve public files before actions
14
14
  server = env['tap.server'] ||= Tap::Server.new
15
15
 
16
- if path = server.public_path(env['PATH_INFO'])
16
+ if path = server.search(:public, env['PATH_INFO'])
17
17
  content = File.read(path)
18
18
  headers = {
19
19
  "Last-Modified" => [File.mtime(path).httpdate],
@@ -35,7 +35,7 @@ module Tap
35
35
 
36
36
  render('index.erb', :locals => {:env => server.env, :env_names => env_names}, :layout => true)
37
37
  end
38
-
38
+
39
39
  def info
40
40
  if request.post?
41
41
  app.info
@@ -0,0 +1,58 @@
1
+ require 'tap/controller'
2
+
3
+ module Tap
4
+ module Controllers
5
+
6
+ # :startdoc::controller remotely controls and monitors server
7
+ #
8
+ # Server provides several uris to control and monitor the server behavior.
9
+ # Importantly, server allows the remote shutdown of a Tap::Server if a
10
+ # shutdown_key is set. This makes it possible to run servers in the
11
+ # background and still have a shutdown handle on them.
12
+ #
13
+ class Server < Tap::Controller
14
+ set :default_layout, 'layout.erb'
15
+
16
+ def index
17
+ render 'index.erb'
18
+ end
19
+
20
+ # Returns 'pong'.
21
+ def ping
22
+ response['Content-Type'] = 'text/plain'
23
+ "pong"
24
+ end
25
+
26
+ # Returns the public server configurations as xml.
27
+ def config
28
+ response['Content-Type'] = 'text/xml'
29
+ %Q{<?xml version="1.0"?>
30
+ <server>
31
+ <uri>#{uri}</uri>
32
+ <shutdown_key>#{shutdown_key}</shutdown_key>
33
+ </server>}
34
+ end
35
+
36
+ # Shuts down the server. Shutdown requires a shutdown key which
37
+ # is setup when the server is launched. If no shutdown key was
38
+ # setup, shutdown does nothing and responds accordingly.
39
+ def shutdown
40
+ if shutdown_key && request['shutdown_key'].to_i == shutdown_key
41
+ # wait a second to shutdown, so the response is sent out.
42
+ Thread.new {sleep 1; server.stop! }
43
+ "shutdown"
44
+ else
45
+ "you do not have permission to shutdown this server"
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ # returns the server shutdown key. the shutdown key is required
52
+ # for shutdown to function, a nil shutdown key disables shutdown.
53
+ def shutdown_key # :nodoc:
54
+ server.shutdown_key
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/tap/server.rb CHANGED
@@ -34,9 +34,9 @@ module Tap
34
34
  # req = Rack::MockRequest.new(server)
35
35
  # req.get('/sample/path/to/resource').body # => "Sample got /sample : /path/to/resource"
36
36
  #
37
- # Server automatically maps unknown keys to a controller by searching
38
- # env.controllers. As a result '/example' maps to the Example controller
39
- # defined in 'lib/example.rb'.
37
+ # Server automatically maps unknown keys to controllers discovered via the
38
+ # env.controllers manifest. The only requirement is that the controller
39
+ # constant is a Rack application. For instance:
40
40
  #
41
41
  # # [lib/example.rb] => %q{
42
42
  # # ::controller
@@ -65,38 +65,116 @@ module Tap
65
65
  #
66
66
  # req.get('/unknown/path/to/resource').body # => "App got : /unknown/path/to/resource"
67
67
  #
68
+ # In development mode, the controller constant is removed and the constant
69
+ # require path is reloaded each time it gets called. This system allows many
70
+ # web frameworks to be hooked into a Tap server.
71
+ #
68
72
  # :::+
69
73
  class Server
74
+ class << self
75
+
76
+ # Instantiates a Server in the specified directory, configured as
77
+ # specified in root/server.yml. If shutdown_key is specified, a
78
+ # random shutdown key will be generated and set on the sever.
79
+ #
80
+ def instantiate(root, shutdown_key=false)
81
+ # setup the server directory
82
+ root = File.expand_path(root)
83
+ FileUtils.mkdir_p(root) unless File.exists?(root)
84
+
85
+ # initialize the server
86
+ app = Tap::App.instance
87
+ env = Tap::Exe.instantiate(root)
88
+ env.activate
89
+ config = Configurable::Utils.load_file(env.root['server.yml'])
90
+
91
+ server = new(env, app, config)
92
+ server.config[:shutdown_key] = rand(1000000) if shutdown_key
93
+ server
94
+ end
95
+
96
+ # Runs the server
97
+ def run(server)
98
+ cookie_server = Rack::Session::Pool.new(server)
99
+ server.run!
100
+ end
101
+ end
102
+
70
103
  include Rack::Utils
71
104
  include Configurable
72
105
 
73
- config :environment, :development
74
- config :server, %w[thin mongrel webrick]
75
- config :host, 'localhost'
76
- config :port, 8080, &c.integer
106
+ config :environment, :development
107
+ config :servers, %w[thin mongrel webrick], &c.list # a list of preferred handlers
108
+ config :host, 'localhost', &c.string # the server host
109
+ config :port, 8080, &c.integer # the server port
77
110
 
78
- config :views_dir, :views
79
- config :public_dir, :public
111
+ # A hash of (key, controller) pairs mapping the controller part of a route
112
+ # to a Rack application. Typically controllers is used to specify aliases
113
+ # when the defaults are not preferable.
80
114
  config :controllers, {}
115
+
116
+ # config :infer_controllers, true, &c.switch
117
+
118
+ # The default controller key used in routes that cannot be directly mapped
119
+ # to a controller
120
+ #--
121
+ # Set to nil to force controller mapping?
81
122
  config :default_controller_key, 'app'
82
123
 
124
+ # Server implements a shutdown key so the server can be shutdown remotely
125
+ # via an HTTP request to the app/shutdown method. Remote shutdown is
126
+ # useful when the user is running a local server (especially from a
127
+ # background process). Under many circumstances remote shutdown is
128
+ # undesirable; specify a nil shutdown key, the default, to turn off
129
+ # shutdown.
130
+ config :shutdown_key, nil, &c.integer_or_nil # specifies a public shutdown key
131
+
83
132
  attr_reader :env
133
+ attr_reader :handler
84
134
 
85
135
  def initialize(env=Env.new, app=Tap::App.instance, config={})
86
136
  @env = env
87
137
  @app = app
88
138
  @cache = {}
139
+ @handler = nil
89
140
  initialize_config(config)
90
141
  end
91
142
 
143
+ # Runs self as configured, on the specified server, host, and port. Use an
144
+ # INT signal to interrupt.
145
+ def run!(handler=rack_handler)
146
+ app.log :run, "#{host}:#{port} (#{handler})"
147
+ handler.run self, :Host => host, :Port => port do |handler_instance|
148
+ @handler = handler_instance
149
+ trap(:INT) { stop! }
150
+ end
151
+ end
152
+
153
+ # Stops the server if running (ie a handler is set). Returns true if the
154
+ # server was stopped, and false otherwise.
155
+ def stop!
156
+ if handler
157
+ # Use thins' hard #stop! if available, otherwise just #stop
158
+ handler.respond_to?(:stop!) ? handler.stop! : handler.stop
159
+ @handler = nil
160
+ false
161
+ else
162
+ true
163
+ end
164
+ end
165
+
92
166
  # Currently a stub for initializing a session. initialize_session returns
93
167
  # an integer session id.
94
168
  def initialize_session
95
169
  id = 0
96
170
  session_app = app(id)
97
- log_path = env.root.prepare(:log, 'server.log')
98
- session_app.logger = Logger.new(log_path)
171
+ session_root = root(id)
99
172
 
173
+ # setup expiration information...
174
+
175
+ # setup a session log
176
+ log_path = session_root.prepare(:log, 'session.log')
177
+ session_app.logger = Logger.new(log_path)
100
178
  session_app.on_complete do |_result|
101
179
  # find the template
102
180
  class_name = _result.key.class.to_s.underscore
@@ -116,28 +194,31 @@ module Tap
116
194
  file << Support::Templater.new(File.read(template)).build(:_result => _result)
117
195
  end
118
196
  end
119
- end
197
+ end unless session_app.on_complete_block
120
198
 
121
199
  id
122
200
  end
123
201
 
124
- # Returns the app provided during initialization. In the future this
125
- # method may be extended to provide a session-specific App, hence it
126
- # has been stubbed with an id input.
202
+ # Returns the session-specific App, or the server app if id is nil.
127
203
  def app(id=nil)
128
204
  @app
129
205
  end
130
206
 
131
- # Returns the env.root provided during initialization. In the future this
132
- # method may be extended to provide a session-specific Root, hence it
133
- # has been stubbed with an id input.
207
+ # Returns the session-specific Root, or the server env.root if id is nil.
134
208
  def root(id=nil)
135
209
  @env.root
136
210
  end
137
211
 
138
- # Returns true if environment is :development.
139
- def development?
140
- environment == :development
212
+ # Returns a uri mapping to the specified controller and action. Parameters
213
+ # may be specified; they are built as a query and attached to the uri as
214
+ # normal.
215
+ #
216
+ # Currenlty uri does not map the controller to a minipath, but in the
217
+ # future it will.
218
+ def uri(controller=nil, action=nil, params={})
219
+ query = build_query(params)
220
+ uri = ["http://#{host}:#{port}", escape(controller), action].compact.join("/")
221
+ query.empty? ? uri : "#{uri}?#{query}"
141
222
  end
142
223
 
143
224
  # The {Rack}[http://rack.rubyforge.org/doc/] interface method.
@@ -173,25 +254,31 @@ module Tap
173
254
  ServerError.response($!)
174
255
  end
175
256
 
176
- #--
177
- # TODO: implement caching for path content
178
- def content(path)
179
- File.read(path)
257
+ # Searches env for the first matching file, directories are not matched.
258
+ def search(dir, path)
259
+ env.search(dir, path) {|file| File.file?(file) }
180
260
  end
181
261
 
182
- #--
183
- # TODO: implement caching for public_paths
184
- def public_path(path)
185
- env.search(public_dir, path) {|public_path| File.file?(public_path) }
186
- end
262
+ protected
187
263
 
188
- #--
189
- # TODO: implement caching for template_paths
190
- def template_path(path)
191
- env.search(views_dir, path) {|template_path| File.file?(template_path) }
264
+ # Returns true if environment is :development.
265
+ def development? # :nodoc:
266
+ environment == :development
192
267
  end
193
268
 
194
- protected
269
+ # Looks up and returns the first available Rack::Handler as listed in the
270
+ # servers configuration. (Note rack_handler returns a handler class, not
271
+ # an instance). Adapted from Sinatra.detect_rack_handler
272
+ def rack_handler # :nodoc:
273
+ servers.each do |server_name|
274
+ begin
275
+ return Rack::Handler.get(server_name)
276
+ rescue LoadError
277
+ rescue NameError
278
+ end
279
+ end
280
+ raise "Server handler (#{servers.join(',')}) not found."
281
+ end
195
282
 
196
283
  # a helper method for routing a key to a controller
197
284
  def lookup(key) # :nodoc:
@@ -213,10 +300,16 @@ module Tap
213
300
  # load the require_path in dev mode so that
214
301
  # controllers will be reloaded each time
215
302
  if development? && const.require_path
216
- if Object.const_defined?(const.const_name)
217
- Object.send(:remove_const, const.const_name)
303
+ parent = if const.nesting.empty?
304
+ Object
305
+ else
306
+ Tap::Support::Constant.constantize(const.nesting) { nil }
218
307
  end
219
-
308
+
309
+ if parent && parent.const_defined?(const.const_name)
310
+ parent.send(:remove_const, const.const_name)
311
+ end
312
+
220
313
  load const.require_path
221
314
  end
222
315
 
@@ -0,0 +1,71 @@
1
+ module Tap
2
+ module Support
3
+
4
+ # A very simple wrapper for root providing a CRUD interface for reading and
5
+ # writing files.
6
+ class Persistence
7
+
8
+ # The Tap::Root for self.
9
+ attr_reader :root
10
+
11
+ # Initializes a new persistence wrapper for the specified root.
12
+ def initialize(root)
13
+ @root = root
14
+ end
15
+
16
+ # Returns the filepath for the specified id. Non-string ids are allowed;
17
+ # they will be converted to strings using to_s.
18
+ def path(id)
19
+ root.subpath(:data, id.to_s)
20
+ end
21
+
22
+ # Returns a list of existing ids.
23
+ def index
24
+ root.glob(:data).select do |path|
25
+ File.file?(path)
26
+ end.collect do |path|
27
+ root.relative_filepath(:data, path)
28
+ end
29
+ end
30
+
31
+ # Creates the file for the specified id. If a block is given, an io to
32
+ # the file will be yielded to it; otherwise the file will be created
33
+ # without content. Returns the path to the persistence file.
34
+ #
35
+ # Raises an error if the file already exists.
36
+ def create(id)
37
+ filepath = path(id)
38
+ raise "already exists: #{filepath}" if File.exists?(filepath)
39
+ root.prepare(filepath) {|io| yield(io) if block_given? }
40
+ end
41
+
42
+ # Reads and returns the data for the specified id, or an empty string if
43
+ # the persistence file doesn't exist.
44
+ def read(id)
45
+ filepath = path(id)
46
+ File.file?(filepath) ? File.read(filepath) : ''
47
+ end
48
+
49
+ # Overwrites the data for the specified id. A block must be given to
50
+ # provide the new content; a persistence file will be created if one
51
+ # does not exist already.
52
+ def update(id)
53
+ root.prepare(path(id)) {|io| yield(io) }
54
+ end
55
+
56
+ # Removes the persistence file for id, if it exists. Returns true if
57
+ # the file was removed.
58
+ def destroy(id)
59
+ filepath = path(id)
60
+
61
+ if File.file?(filepath)
62
+ FileUtils.rm(filepath)
63
+ true
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -40,5 +40,5 @@
40
40
  <% end %>
41
41
  </dd>
42
42
  <% end %>
43
- </dl>
43
+ </dl>
44
44
  </div>
@@ -0,0 +1,3 @@
1
+ <% if shutdown_key %>
2
+ <a href="<%= uri(:shutdown) %>?shutdown_key=<%= shutdown_key %>">shutdown</a>
3
+ <% end %>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tap-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Chiang
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-07 00:00:00 -07:00
12
+ date: 2009-03-17 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -47,10 +47,12 @@ files:
47
47
  - lib/tap/controller.rb
48
48
  - lib/tap/controllers/app.rb
49
49
  - lib/tap/controllers/schema.rb
50
+ - lib/tap/controllers/server.rb
50
51
  - lib/tap/server.rb
51
52
  - lib/tap/server_error.rb
52
53
  - lib/tap/tasks/echo.rb
53
54
  - lib/tap/tasks/server.rb
55
+ - lib/tap/support/persistence.rb
54
56
  - public/javascripts/prototype.js
55
57
  - public/javascripts/tap.js
56
58
  - public/stylesheets/tap.css
@@ -70,6 +72,7 @@ files:
70
72
  - views/tap/controllers/schema/preview.erb
71
73
  - views/tap/controllers/schema/round.erb
72
74
  - views/tap/controllers/schema/schema.erb
75
+ - views/tap/controllers/server/index.erb
73
76
  - views/tap/tasks/echo/result.html
74
77
  - README
75
78
  - MIT-LICENSE