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!