/
sytes.lisp
358 lines (313 loc) · 13.4 KB
/
sytes.lisp
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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
;;;; sytes.lisp
(in-package #:sytes)
(export '(register-syte
unregister-syte
syte-request-handler
syte-send-as-static
reset-caches
def-url-handler
url-to-file))
;;; "sytes" goes here. Hacks and glory await!
(defparameter *syte-names* (make-hash-table :test #'equal))
(defparameter *current-syte* nil)
(defparameter *syte-toplevel-context* (tmpl:make-context :name "SYTES"))
(defun reset-caches ()
(tmpl:clear-cache))
(defmacro report-time-spent (name &body body)
(let ((t1 (gensym)))
`(let ((,t1 (get-internal-real-time)))
(prog1
(progn ,@body)
(tbnl:log-message* :TIME "~A: ~3$s"
,name
(/ (- (get-internal-real-time) ,t1)
internal-time-units-per-second))))))
(with-open-file (in (merge-pathnames "template/instance.syt"
(asdf:component-pathname
(asdf:find-system :sytes))))
(funcall (tmpl::compile (tmpl::parse in) :context *syte-toplevel-context*)
*syte-toplevel-context*))
(defclass syte ()
((names :accessor syte-names
:initarg :names)
(root :accessor syte-root
:initarg :root)
(context :accessor syte-context
:initarg :context)
(url-handlers :accessor syte-url-handlers
:initarg :url-handlers
:initform nil))
(:default-initargs
:names (error "Syte names are missing")
:root (error "Syte root is missing")))
(defun maybe-exec-boot (syte &key
(boot ".boot.syt")
(root (syte-root syte)))
(let ((boot (merge-pathnames boot root)))
(when (probe-file boot)
(let ((ctx (syte-context syte)))
(multiple-value-bind (tmpl was-cached)
(tmpl::compile-file boot
:parent-context ctx
:sub-context ctx)
(unless was-cached
(funcall (tmpl:template-function tmpl) ctx)))))))
(defmethod initialize-instance :after ((syte syte) &key names root context &allow-other-keys)
(unless context
(setf root
(setf (syte-root syte) (fad:pathname-as-directory (truename root))))
(setf context
(setf (syte-context syte) (tmpl:make-context :name (car names) :root root :parent *syte-toplevel-context*))))
(tmpl:defglobal-context (tmpl:tops "%filters%") (make-hash-table :test #'equal) context))
(defun register-syte (syte)
(loop for name in (syte-names syte)
do (setf (gethash name *syte-names*) syte)))
(defun unregister-syte (syte)
(loop for name in (syte-names syte)
do (remhash name *syte-names*)))
(defun url-to-file (file &optional (syte *current-syte*))
(let* ((root (syte-root syte))
(pos (position-if (lambda (x) (char/= x #\/)) file))
(redirect nil))
(if pos
(setf file (subseq file pos))
(setf file ""))
(setf file (merge-pathnames file root))
(cond
((fad:directory-exists-p file)
(setf redirect (not (fad:directory-pathname-p file))
file (merge-pathnames "index.syt"
(fad:pathname-as-directory file))))
(t
(when (fad:directory-pathname-p file)
(setf file (fad:pathname-as-file file)))
(unless (probe-file file)
(setf file (make-pathname :defaults file :type "syt")))))
(values file redirect)))
(defgeneric syte-locate-template (syte request)
(:method ((syte syte) request)
(let ((script (tbnl:script-name request)))
(url-to-file script syte))))
(defgeneric syte-send-as-static (syte request file)
(:method ((syte syte) request file)
(declare (ignore syte request file))
nil))
(defgeneric syte-request-handler (syte request)
(:method ((syte (eql nil)) request)
(format nil "No syte defined for host ~A" (hostname request)))
(:method ((syte syte) request)
(maybe-exec-boot syte)
(let ((file)
(redirect)
(script (tbnl:script-name request))
(moarvars)
(forbidden))
(loop for (regexp . handler) in (syte-url-handlers syte) do
(multiple-value-bind (match-start match-end reg-starts reg-ends)
(ppcre:scan regexp script)
(declare (ignore match-start match-end))
(when reg-starts
(let* ((registers (map 'list (lambda (start end)
(when (and start end)
(subseq script start end)))
reg-starts reg-ends))
(result (apply handler registers)))
(unless (eq result :continue)
(destructuring-bind (&key
variables template redirect
content static-file
content-type content-disposition
status) result
(when redirect
(tbnl:redirect redirect))
(when static-file
(return-from syte-request-handler
(tbnl:handle-static-file static-file content-type)))
(when content-type
(setf (tbnl:content-type*) content-type))
(when content-disposition
(setf (tbnl:header-out :content-disposition) content-disposition))
(when status
(setf (tbnl:return-code*) status))
(when content
(return-from syte-request-handler content))
(if template
(multiple-value-setq (file redirect)
(url-to-file template syte)))
(when variables
(setf moarvars variables))
(return)))))))
(unless file
(multiple-value-setq (file redirect)
(syte-locate-template syte request))
(when redirect
(tbnl:redirect (format nil "~A/" (tbnl:script-name request))))
(setf forbidden (let ((filename (pathname-name file)))
(or (and (> (length filename) 0)
(char= #\. (char filename 0)))
(and (string-equal (pathname-type file) "syt")
(or (string-equal filename "autohandler")
(string-equal filename "dhandler")))))))
(awhen (syte-send-as-static syte request file)
(return-from syte-request-handler
(tbnl:handle-static-file file (when (stringp it) it))))
(let ((*package* (find-package :sytes.%runtime%)))
(tmpl:exec-template-request file (syte-root syte) (syte-context syte)
:variables moarvars
:forbidden forbidden)))))
(defmacro def-url-handler ((syte regexp &rest args) &body body)
(with-rebinds (syte regexp)
`(setf (syte-url-handlers ,syte)
(acons ,regexp (lambda (&optional ,@args)
(declare (ignorable ,@args))
,@body)
(syte-url-handlers ,syte)))))
(defmethod syte-request-handler :around ((syte syte) request)
(report-time-spent (format nil "~A~A"
(car (syte-names syte))
(tbnl:script-name request))
(call-next-method)))
;;; hunchentoot stuff
(defclass sytes-request (tbnl:request)
())
(defclass sytes-acceptor (tbnl:acceptor)
()
(:default-initargs
:error-template-directory nil
:access-log-destination "/tmp/sytes.log"
:message-log-destination "/tmp/sytes.messages"
:port 7379
:request-class 'sytes-request))
(defparameter *acceptor* nil)
(defun start-server (&key (port 7379))
(setf *acceptor* (make-instance 'sytes-acceptor :port port))
(tbnl:start *acceptor*))
(defun stop-server ()
(tbnl:stop *acceptor*)
(setf *acceptor* nil))
(defgeneric hostname (request)
(:method ((request sytes-request))
(let* ((host (tbnl:host request))
(split (split-sequence #\: host)))
(car split))))
(defmethod tbnl:acceptor-dispatch-request ((acceptor sytes-acceptor) request)
(setf (tbnl:content-type*) "text/html; charset=UTF-8")
;; (tbnl:start-session)
(let* ((host (hostname request))
(syte (gethash host *syte-names*))
(*current-syte* syte))
(syte-request-handler syte request)))
(defmethod tbnl:acceptor-status-message ((acceptor sytes-acceptor) code &key &allow-other-keys)
(unless (= code 404)
(call-next-method)))
;;; some more primitives
(labels
((getpath (name &optional (current tmpl:*current-template*))
(let* ((path (pathname name))
(dir (pathname-directory path)))
(if (eq (car dir) :absolute)
(let ((path (make-pathname :defaults path
:directory (list* :relative (cdr dir)))))
(merge-pathnames path (syte-root *current-syte*)))
(merge-pathnames path (tmpl:template-filename current)))))
(process (name defs)
(let* ((tmpl (tmpl:compile-file (getpath name)))
(ctx (tmpl:make-context :name (tmpl:template-filename tmpl)
:parent tmpl::*current-context*)))
(values (apply (tmpl:template-function tmpl) ctx defs)
ctx))))
(tmpl:def-primitive "require"
(lambda (name &rest defs)
(multiple-value-bind (text ctx)
(process name defs)
(declare (ignore text))
ctx)))
(tmpl:def-primitive "process"
(lambda (name &rest defs)
(process name defs)))
(tmpl:def-primitive "include"
(lambda (name)
(let ((filename (getpath name)))
(when (fad:file-exists-p filename)
(read-whole-file-utf8 filename))))))
(tmpl:def-primitive "absurl"
(lambda (relink)
(destructuring-bind (relink &optional hash)
(ppcre:split "#" relink :limit 2)
(with-output-to-string (out)
(let* ((current tmpl:*current-template*)
(filename (tmpl:template-filename current))
(directory (make-pathname :directory (pathname-directory filename)))
(root (syte-root *current-syte*))
(relbase (list* :absolute
(cdr (pathname-directory (enough-namestring filename root)))))
(absolute-url (merge-pathnames relink (make-pathname :directory relbase)))
(absolute-file (merge-pathnames relink directory)))
(write-string (namestring
(cond
((fad:directory-exists-p absolute-file)
(fad:pathname-as-directory absolute-url))
((and (fad:file-exists-p absolute-file)
(awhen (pathname-type absolute-file)
(string-equal it "syt")))
(let ((name (pathname-name absolute-url)))
(make-pathname :directory (pathname-directory absolute-url)
:name (unless (string= name "index") name))))
(t
absolute-url))) out)
(when hash
(write-char #\# out)
(write-string hash out)))))))
(tmpl:def-primitive "sameurl"
(lambda (url1 &optional url2)
(let ((file1 (url-to-file url1))
(file2 (if url2
(url-to-file url2)
(tmpl:template-filename tmpl:*request-template*))))
(macrolet
((canonic (name)
`(progn
(when (fad:file-exists-p ,name)
(setf ,name (truename ,name)))
(when (pathnamep ,name)
(setf ,name (namestring ,name))))))
(canonic file1)
(canonic file2)
(let ((dir (namestring (make-pathname :directory (pathname-directory file1)))))
(cons (equal file1 file2)
(starts-with file2 dir)))))))
(tmpl:def-primitive "http/set-status"
(lambda (status)
(when (symbolp status)
(setf status (symbol-name status)))
(when (stringp status)
(let* ((name (format nil "+HTTP-~A+" (string-upcase status)))
(sym (find-symbol name :hunchentoot)))
(when sym (setf status (symbol-value sym)))))
(setf (tbnl:return-code*) status)))
(tmpl:def-primitive "http/set-content-type"
(lambda (ct)
(setf (tbnl:content-type*) ct)))
(tmpl:def-primitive "http/script-name"
(lambda ()
(tbnl:script-name*)))
(tmpl:def-primitive "http/parameter"
(lambda (name)
(tbnl:parameter name)))
(defun def-syte-primitive (syte name func)
(tmpl:def-primitive name func (syte-context syte)))
;;; entry point for buildapp
#+sbcl
(progn
(defparameter *running* t)
(defun main (argv)
(declare (ignore argv))
(sb-daemon:daemonize :output "/tmp/sytes.output"
:error "/tmp/sytes.error"
:exit-parent t
:sigterm (lambda (sig)
(declare (ignore sig))
(setf *running* nil)))
(setf *running* t)
(start-server)
(loop while *running* do (sleep 1))))