aboutsummaryrefslogtreecommitdiffstats
path: root/build/server.moon
blob: a19edb087fc6f5f3540165106dd4767d82a901b6 (plain)
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
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, :path
        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 == '?'
              facet.type = 'text/html'
              current = fileder
              while current
                if type = current\get '_web_view: type'
                  facet.type = type\match '^%s*(.-)%s*$'
                  break

                if current.path == ''
                  break

                path, _ = dir_base current.path
                current = root\walk path

            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 (not type or type == '') and method ~= 'GET' and method ~= 'HEAD'
      nil
    else
      type or= '?'
      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!