add = (tmpl) ->
package.path ..= ";#{tmpl}.lua"
package.moonpath ..= ";#{tmpl}.moon"
add '?'
add '?.server'
add '?/init'
add '?/init.server'
require 'mmm'
import dir_base, Key, Fileder from require 'mmm.mmmfs.fileder'
import convert, MermaidDebugger from require 'mmm.mmmfs.conversion'
import get_store from require 'mmm.mmmfs.stores'
import render from require 'mmm.mmmfs.layout'
import Browser from require 'mmm.mmmfs.browser'
import decodeURI from require 'http.util'
lfs = require 'lfs'
server = require 'http.server'
headers = require 'http.headers'
class Server
new: (@store, opts={}) =>
opts = {k,v for k,v in pairs opts}
opts.host = 'localhost' unless opts.host
opts.port = 8000 unless opts.port
opts.onstream = @\stream
opts.onerror = @\error
@server = server.listen opts
@flags = opts.flags
if @flags.rw == nil
@flags.rw = opts.host == 'localhost' or opts.host == '127.0.0.1'
if @flags.unsafe == nil
@flags.unsafe = not @flags.rw or opts.host == 'localhost' or opts.host == '127.0.0.1'
export UNSAFE
UNSAFE = @flags.unsafe
require 'mmm.mmmfs'
listen: =>
assert @server\listen!
_, ip, port = @server\localname!
print "[#{@@__name}]",
"running at #{ip}:#{port}",
"[#{table.concat [flag for flag,on in pairs @flags when on], ', '}]"
assert @server\loop!
handle_interactive: (fileder, facet) =>
root = Fileder @store
browser = Browser root, fileder.path, facet.name
deps = [[
]]
render browser\todom!, fileder, noview: true, scripts: deps .. "
"
handle_debug: (fileder, facet) =>
debugger = MermaidDebugger!
fileder\find facet, nil, nil, nil, debugger
convert 'text/mermaid-graph', 'text/html', debugger\render!, fileder, facet.name
handle: (method, path, facet, value) =>
if not @flags.rw and method != 'GET' and method != 'HEAD'
return 403, 'editing not allowed'
switch method
when 'GET', 'HEAD'
root = Fileder @store
export BROWSER
BROWSER = :root
fileder = root\walk path -- Fileder @store, path
if not fileder
-- fileder not found
return 404, "fileder '#{path}' not found"
val = switch facet.name
when '?index', '?tree'
-- serve fileder index
-- '?index': one level deep
-- '?tree': recursively
depth = if facet.name == '?tree' then -1 else 1
index = @store\get_index path, depth
convert 'table', facet.type, index, fileder, facet.name
else
if facet.type == 'text/html+interactive'
@handle_interactive fileder, facet
else if base = facet.type\match '^DEBUG %-> (.*)'
facet.type = base
@handle_debug fileder, facet
else if not fileder\has_facet facet.name
404, "facet '#{facet.name}' not found in fileder '#{path}'"
else
fileder\get facet
if val
200, val
else
406, "cant convert facet '#{facet.name}' to '#{facet.type}'"
when 'POST'
if facet
@store\create_facet path, facet.name, facet.type, value
200, 'ok'
else
200, @store\create_fileder dir_base path
when 'PUT'
if facet
@store\update_facet path, facet.name, facet.type, value
200, 'ok'
else
cmd, args = value\match '^([^\n]+)\n(.*)'
switch cmd
when 'swap'
child_a, child_b = args\match '^([^\n]+)\n([^\n]+)$'
assert child_a and child_b, "invalid arguments"
@store\swap_fileders path, child_a, child_b
200, 'ok'
when nil
400, "invalid request"
else
501, "unknown command #{cmd}"
when 'DELETE'
if facet
@store\remove_facet path, facet.name, facet.type
else
@store\remove_fileder path
200, 'ok'
else
501, "not implemented"
err_and_trace = (msg) -> debug.traceback msg, 2
stream: (sv, stream) =>
req = stream\get_headers!
method = req\get ':method'
path = req\get ':path'
path = decodeURI path
path_facet, type = path\match '(.*):(.*)'
path_facet or= path
path, facet = path_facet\match '(.*)/([^/]*)'
facet = if facet == '' and type == '' and method ~= 'GET' and method ~= 'HEAD'
nil
else
type or= 'text/html+interactive'
type = type\match '%s*(.*)'
Key facet, type
value = stream\get_body_as_string!
ok, status, body = xpcall @.handle, err_and_trace, @, method, path, facet, value
if not ok
warn "Error handling request (#{method} #{path} #{facet}):\n#{status}"
body = "Internal Server Error:\n#{status}"
status = 500
res = headers.new!
response_type = if status > 299 then 'text/plain'
else if facet and facet.type == 'text/html+interactive' then 'text/html'
else if facet then facet.type
else 'text/plain'
res\append ':status', tostring status
res\append 'content-type', response_type
stream\write_headers res, method == 'HEAD'
if method ~= 'HEAD'
stream\write_chunk body, true
error: (sv, ctx, op, err, errno) =>
msg = "#{op} on #{tostring ctx} failed"
msg = "#{msg}: #{err}" if err
-- usage:
-- moon server.moon [FLAGS] [STORE] [host] [port]
-- * FLAGS - any of the following:
-- --[no-]rw - enable/disable POST?PUT/DELETE operations (default: on if local)
-- --[no-]unsafe - enable/disable server-side code execution when writable is on (default: on if local or --no-rw)
-- * STORE - see mmm/mmmfs/stores/init.moon:get_store
-- * host - interface to bind to (default localhost, set to 0.0.0.0 for public hosting)
-- * port - port to serve from, default 8000
flags = {}
arguments = for a in *arg
if flag = a\match '^%-%-no%-(.*)$'
flags[flag] = false
continue
elseif flag = a\match '^%-%-(.*)$'
flags[flag] = true
continue
else
a
{ store, host, port } = arguments
store = get_store store
server = Server store, :flags, :host, port: port and tonumber port
server\listen!