1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
|
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 = [[
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.6/svg.min.js"></script>
<script type="text/javascript" src="//unpkg.com/mermaid@8.4.0/dist/mermaid.min.js"></script>
<script type="text/javascript" src="//unpkg.com/marked@0.7.0/marked.min.js"></script>
<link rel="stylesheet" type="text/css" href="//unpkg.com/codemirror@5.49.2/lib/codemirror.css" />
<script type="text/javascript" src="//unpkg.com/codemirror@5.49.2/lib/codemirror.js"></script>
<script type="text/javascript" src="//unpkg.com/codemirror@5.49.2/mode/lua/lua.js"></script>
<script type="text/javascript" src="//unpkg.com/codemirror@5.49.2/mode/markdown/markdown.js"></script>
<script type="text/javascript" src="//unpkg.com/codemirror@5.49.2/addon/display/autorefresh.js"></script>
<script type="text/javascript" src="/static/fengari-web/:text/javascript"></script>
<script type="text/lua" src="/static/mmm/:text/lua"></script>
<script type="text/lua">require 'mmm'; require 'mmm.mmmfs'</script>]]
render browser\todom!, fileder, noview: true, scripts: deps .. "
<script type=\"text/lua\">
on_load = on_load or {}
table.insert(on_load, function()
local path = #{string.format '%q', fileder.path}
local facet = #{string.format '%q', facet.name}
local browser = require 'mmm.mmmfs.browser'
local fileder = require 'mmm.mmmfs.fileder'
local web = require 'mmm.mmmfs.stores.web'
local store = web.WebStore({ verbose = true })
local root = fileder.Fileder(store, store:get_index(nil, -1))
local err_and_trace = function (msg) return debug.traceback(msg, 2) end
local ok, browser = xpcall(browser.Browser, err_and_trace, root, path, facet, true)
if not ok then error(browser) end
end)
</script>"
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!
|