-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfile-response.lsp
222 lines (198 loc) · 9.59 KB
/
file-response.lsp
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
;; File responses
;; Serving VMS files as http responses is not quite as straightforward as
;; one would be used from Unix as one has to deal with the various file
;; structures that the VMS file system provides. We have to distinguish
;; between binary and text files and treat them separately. Binary files
;; are sent to the client as-is, i.e. the data is read block-wise from
;; the file and sent to the client unmodified. As VAX LISP provides no
;; facilities for efficient block-wise binary I/O, we're using the VMS
;; Record Management System (RMS) directly. For text files, we use the
;; VAX LISP file I/O system and read files line-by-line and send them
;; to the client with some intermediate buffering.
(in-package :rasselbock)
(provide 'file-response)
(eval-when (compile load eval)
(require 'rmsdef)
(require 'rmsusr)
(require 'utils)
(require 'vms)
(require 'response))
(defun make-subdirectory (pathname subdirectory)
(apply #'concatenate 'string
(directory-namestring pathname)
(when subdirectory
(list "." subdirectory))))
;; Document root directory - This is set at run time from either the logical
;; name RASSELBOCK$DOCUMENT_ROOT or, if that is not defined, to the [.PUBLIC]
;; subdirectory of the process' default directory.
(defparameter *document-root* nil)
(defun get-document-root ()
(let ((translations (translate-logical-name "RASSELBOCK$DOCUMENT_ROOT"))
(default-document-root (make-pathname
:directory (make-subdirectory
(default-directory) "PUBLIC"))))
(cond
((> (length translations) 1)
(error "RASSELBOCK$DOCUMENT_ROOT translates to multiple directories, ~
which is not supported."))
((= (length translations) 1)
(pathname (first translations)))
(t
(warn "RASSELBOCK$DOCUMENT_ROOT not defined, serving files from ~A"
(namestring default-document-root))
default-document-root))))
(defun ensure-document-root ()
(unless *document-root*
(setf *document-root* (get-document-root))))
(defun document-root ()
(c sys$dclast
common-ast-address
(instate-interrupt-function #'ensure-document-root :once-only-p t)
0)
*document-root*)
;; Provide some syntactic sugar to make using the GET-FILE-INFORMATION
;; function more pleasant.
(defmacro with-file-information ((file &rest fields) &body body)
"Call GET-FILE-INFORMATION with FILE and FIELDS as arguments. FIELDS
must be one or more symbols that will be bound during the evaluation of
BODY. They specify what fields of file information should be retrieved
and are converted to keywords when GET-FILE-INFORMATION is invoked."
(let ((keywords (mapcar #'make-keyword fields))
(result (gensym)))
`(let* ((,result (get-file-information ,file ,@keywords))
,@(mapcar #'(lambda (variable keyword)
(list variable `(getf ,result ,keyword)))
fields (mapcar #'make-keyword fields)))
,@body)))
;; Map from file type (extension) to content type
(defparameter *content-types* (list :jpg "image/jpeg"
:jpeg "image/jpeg"
:html "text/html"
:xml "text/xml"
:js "text/javascript"))
(defun make-content-type-map ()
(let ((map (make-hash-table)))
(doplist (type content-type *content-types*)
(setf (gethash type map) content-type))
map))
(defparameter *content-type-map* (make-content-type-map))
;; The FILE structure contains information about the file being served. When
;; a request is found to refer to a file, the body field of the response
;; returned by the file handler will contain a FILE structure that is then
;; used when writing the body to the client.
(defstruct file pathname size textp content-type)
(defun file-information (pathname)
"Return information about the given file in a file structure. FILE-SIZE
is the size of the file in bytes. PATHNAME is the pathname argument passed
to this function. FILE-TEXTP has a true value if the file is a
text file, based on RMS attribute heuristics. FILE-CONTENT-TYPE is the content
type to report to the client."
;; FIXME: file-size only works for fixed-block-size, non-indexed files
(with-file-information (pathname organization record-format record-attributes
block-size end-of-file-block first-free-byte)
(when organization
(let ((textp (and (= organization FAB$C_SEQ)
(or (= record-format FAB$C_VAR)
(= record-format FAB$C_VFC))
(not (zerop (logand record-attributes
(logior FAB$M_CR FAB$M_PRN)))))))
(make-file :pathname pathname
:size (+ (* block-size (1- end-of-file-block)) first-free-byte)
:textp textp
:content-type (or (gethash (make-keyword (pathname-type pathname))
*content-type-map*)
(if textp
"text/plain"
"application/binary")))))))
(defun write-binary-file-to-socket (channel filename)
"Serve a binary file to the client, using RMS to read blocks of file data
directly into the buffer sent to the client using $QIOW."
(let* ((namestring (namestring filename))
(filename-buffer (make-filename-buffer :allocation :static
:filename namestring))
(fab (make-fab :allocation :static
:FAB$V_GET 1
:FAB$V_BIO 1
:FAB$V_SHRGET 1
:FAB$L_FNA filename-buffer
:FAB$B_FNS (length namestring)))
(buffer (make-string-buffer :allocation :static))
(rab (make-rab :allocation :static
:RAB$L_FAB fab
:RAB$L_UBF buffer
:RAB$W_USZ +buffer-size+)))
(c SYS$OPEN fab 0 0)
(c SYS$CONNECT rab 0 0)
(loop
(let ((status (call-out SYS$READ rab 0 0)))
(when (= status RMS$_EOF)
(return))
(check-status status 'SYS$READ)
($QIOW/check-iosb channel IO$_WRITEVBLK
:p1 buffer
:p2 (RAB$W_RSZ rab))))
(c SYS$CLOSE fab 0 0)))
(defun write-text-file-to-socket (channel filename)
"Serve a text file to the client. The file is read line-wise into a
statically allocated string buffer. Whenever the buffer would be overflown
by the next line read from the file, it is flushed to the client. This
is supposed to reduce the number of packets sent to the client under the
assumption that user-level buffering is cheaper than leaving this up to
the TCP stack."
(let* ((buffer (make-array +buffer-size+
:allocation :static
:fill-pointer 0
:adjustable t
:element-type 'character)))
(with-open-file (in filename)
(with-output-to-string (*standard-output* buffer)
(loop
(let ((line (read-line in nil)))
(when (null line)
(return))
(when (> (length line) (- +buffer-size+ 2))
(error "line in file ~A too long, cannot serve as text file" filename))
(when (> (+ (length buffer) (length line) 2) +buffer-size+)
($QIOW/check-iosb channel IO$_WRITEVBLK
:p1 buffer
:p2 (length buffer))
(setf (fill-pointer buffer) 0)
(get-output-stream-string *standard-output*))
(format t "~A~C~C" line #\return #\linefeed)))))
(when (plusp (length buffer))
($QIOW/check-iosb channel IO$_WRITEVBLK
:p1 buffer
:p2 (length buffer)))))
(defun write-file-response (channel file)
"Write a file response body, dispatching to binary and text file writing
as appropriate."
(funcall (if (file-textp file)
#'write-text-file-to-socket
#'write-binary-file-to-socket)
channel (file-pathname file)))
(defun uri-to-pathname (uri)
"Return pathname represented by URI"
(let ((dot-position (position #\. uri :from-end t))
(slash-position (position #\/ uri :from-end t)))
(when (and dot-position
(> dot-position slash-position))
(make-pathname
:name (subseq uri (1+ slash-position) dot-position)
:type (subseq uri (1+ dot-position))
:directory (make-subdirectory (document-root)
(when (plusp slash-position)
(substitute #\. #\/
(subseq uri 1 slash-position))))
:defaults (document-root)))))
(defstruct (file-response (:include response
(write-body #'write-file-response))))
(defun route-as-file (request)
"If URI in the given REQUEST structure refers to a file in the file system,
return a FILE-RESPONSE structure."
(let* ((pathname (probe-file (uri-to-pathname (request-uri request))))
(file (file-information pathname)))
(when pathname
(make-file-response :status 200
:status-string "OK"
:header (list :content-type (file-content-type file))
:body file))))