aboutsummaryrefslogtreecommitdiffstats
path: root/build/server.moon
blob: 88d59867b813fb8c8950a43943c2fd438a51aa50 (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
add = (tmpl) ->
  package.path ..= ";#{tmpl}.lua"
  package.moonpath ..= ";#{tmpl}.moon"

add '?'
add '?.server'
add '?/init'
add '?/init.server'

require 'mmm'

export UNSAFE
UNSAFE = true

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 Browser from require 'mmm.mmmfs.browser'
import init_cache from require 'mmm.mmmfs.cache'
import decodeURI from require 'http.util'

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

    @server = server.listen opts

    @flags = opts.flags

    if @flags.unsafe == nil
      @flags.unsafe = opts.host == 'localhost' or opts.host == '127.0.0.1'

    if @flags.cache
      @root = Fileder @store
      init_cache!

    if @flags['http-cache']
      root = @root or Fileder @store
      @revision = root\get 'revision: git/hash'
      @revision or= os.date!
      @revision = @revision\gsub '%s+$', ''

    -- @TODO: fix UNSAFE!
    UNSAFE = @flags.unsafe

  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 = @root or Fileder @store
    browser = Browser root, fileder.path, facet.name

    convert 'mmm/dom+interactive', 'text/html', browser\todom!, fileder, facet.name

  handle_debug: (fileder, facet) =>
    debugger = MermaidDebugger!
    fileder\find facet, nil, nil, nil, debugger
    print debugger\render!
    convert 'text/mermaid-graph', 'text/html', debugger\render!, fileder, facet.name

  handle: (method, path, facet, value) =>
    if method != 'GET' and method != 'HEAD'
      return 501, "not implemented"

    root = @root or Fileder @store
    export BROWSER
    BROWSER = :path
    fileder = root\walk 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
          return 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}'"

  get_etag: (path, facet) =>
    return unless @revision

    facet = tostring facet

    path = path\gsub '"', '_'
    facet = facet\gsub '"', '_'

    etag = "#{@revision}:#{path}: #{facet}"
    string.format '%q', etag

  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, typ = path\match '(.*):(.*)'
    path_facet or= path
    path, facet = path_facet\match '(.*)/([^/]*)'

    facet = if facet == '' and (not typ or typ == '') and method ~= 'GET' and method ~= 'HEAD'
      nil
    else
      typ or= '?'
      typ = typ\match '%s*(.*)'
      Key facet, typ

    res = headers.new!
    etag = @get_etag path, facet
    if etag
      res\append 'etag', etag

      if etag == req\get 'if-none-match'
          res = headers.new!
          res\append ':status', tostring 304
          stream\write_headers res, true
          return

    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

    response_type = if status > 299 then 'text/plain'
    elseif facet and facet.type == 'text/html+interactive' then 'text/html'
    elseif facet then facet.type
    else 'text/plain'
    res\append ':status', tostring status
    res\append 'content-type', response_type

    ok = stream\write_headers res, method == 'HEAD'
    if ok and method ~= 'HEAD'
      stream\write_chunk body, true

-- usage:
-- moon server.moon [FLAGS] [STORE] [host] [port]
-- * FLAGS - any of the following:
--   --[no-]unsafe     - enable/disable server-side code execution when writable is on (default: on if local)
--   --[no-]cache      - cache all fileder contents                                    (default: off)
--   --[no-]http-cache - use ETag and Last-Modified headers                            (default: off)
-- * 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!