-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathstatic.lua
268 lines (251 loc) · 7.67 KB
/
static.lua
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
--
-- static file server
--
local Table = require('table')
local UV = require('uv_native')
local uv = require('uv')
local Fs = require('fs')
local get_type = require('mime').getType
local date = require('os').date
local resolve = require('path').resolve
local parse_url = require('url').parse
local function noop() end
--
-- open file `path`, seek to `offset` octets from beginning and
-- read `size` subsequent octets.
-- call `progress` on each read chunk
--
local function stream_file(path, offset, size, progress, callback, CHUNK_SIZE)
CHUNK_SIZE = CHUNK_SIZE or 4096
UV.fsOpen(path, 'r', '0666', function (err, fd)
if err then
callback(err)
return
end
local readchunk
readchunk = function (err)
if err then
callback(err)
UV.fsClose(fd, noop)
return
end
-- FIXME: disk optimization: the very first read() should read data up to
-- the start of next CHUNK_SIZE, so that subsequent reads be aligned?
local chunk_size = size < CHUNK_SIZE and size or CHUNK_SIZE
UV.fsRead(fd, offset, chunk_size, function (err, chunk)
if err or #chunk == 0 then
callback(err)
UV.fsClose(fd, noop)
else
chunk_size = #chunk
offset = offset + chunk_size
size = size - chunk_size
if progress then
progress(chunk, readchunk)
else
readchunk()
end
end
end)
end
readchunk()
end)
end
--
-- setup request handler
--
local function setup(mount, options)
options = options or { }
-- given Range: header, return start, end numeric pair
local function parse_range(range, size)
local partial, start, stop = false
-- parse bytes=start-stop
if range then
start, stop = range:match('bytes=(%d*)-?(%d*)')
partial = true
end
start = tonumber(start) or 0
stop = tonumber(stop) or size - 1
return start, stop, partial
end
-- cache entries table
local cache = { }
-- handler for 'change' event of all file watchers
local function invalidate_cache_entry(event, path)
-- invalidate cache entry and free the watcher
if cache[path] then
local entry = cache[path]
cache[path] = nil
entry.watch:close()
entry.watch = nil
end
end
-- given file, serve contents, honor Range: header
local function serve(self, file, range, cache_it)
-- adjust headers
local headers = { }
for k, v in pairs(file.headers) do headers[k] = v end
local size = file.size
local start = 0
local stop = size - 1
-- range specified? adjust headers and http status for response
if range then
-- limit range by file size
start, stop = parse_range(range, size)
-- check range validity
if stop >= size then
stop = size - 1
end
if stop < start then
self:writeHead(416, {
['Content-Range'] = 'bytes */' .. file.size
})
self:finish()
return
end
-- adjust Content-Length:
headers['Content-Length'] = stop - start + 1
-- append Content-Range:
headers['Content-Range'] = ('bytes %d-%d/%d'):format(start, stop, size)
self:writeHead(206, headers)
else
self:writeHead(200, headers)
end
-- serve from cache, if available
if file.data then
self:finish(
range and file.data.sub(start + 1, stop - start + 1) or file.data
)
-- otherwise stream and possibly cache
else
-- N.B. don't cache if range specified
if range then
cache_it = false
end
local index, parts = 1, { }
-- called when file chunk is served
local function progress(chunk, cb)
if cache_it then
parts[index] = chunk
index = index + 1
end
self:write(chunk, cb)
end
-- called when file is served
local function eof(err)
self:finish()
if cache_it then
file.data = Table.concat(parts, '')
end
end
stream_file(
file.name,
start,
stop - start + 1,
progress,
eof,
options.chunkSize
)
end
end
-- cache some locals
local fstat = options.follow and Fs.stat or Fs.lstat
local max_age = options.maxAge or 0
local root = options.root
local uri_skip = #mount + 1
local auto_index = options.autoIndex
local redirect_folders = auto_index and options.redirectFolders ~= false
local is_cacheable = options.isCacheable or noop
--
-- request handler
--
return function (req, res, nxt)
-- none of our business unless method is GET
-- or uri doesn't start with `mount`
local uri = req.uri or parse_url(req.url)
if req.method ~= 'GET' or uri.pathname:find(mount, 1, true) ~= 1 then
nxt()
return
end
-- map url to local filesystem filename
-- TODO: Path.normalize(req.url)
local filename = resolve(root, uri.pathname:sub(uri_skip))
-- filename ends with / and auto_index allowed?
if auto_index and filename:sub(-1, -1) == '/' then
-- append auto_index to filename
filename = filename .. auto_index
end
-- stream file, possibly caching the contents for later reuse
local file = cache[filename]
-- no need to serve anything if file is cached at client side
if file
and file.headers['Last-Modified'] == req.headers['if-modified-since']
then
res:writeHead(304, file.headers)
res:finish()
return
end
if file then
serve(res, file, req.headers.range, false)
else
fstat(filename, function (err, stat)
-- filename not found? proceed to next layer
if err then nxt() ; return end
-- filename is symlink and follow symlinks is disabled? not found
if stat.is_symbolic_link and not options.follow then nxt(); return end
-- filename is directory?
if stat.is_directory then
-- redirection is not turned off?
if redirect_folders then
-- append / to the pathname and redirect there
res:writeHead(301, { Location = uri.pathname .. '/' .. uri.search })
res:finish()
-- proceed to the next layer
else
nxt()
end
return
end
-- create cache entry, even for files which contents are not
-- gonna be cached
-- collect information on file
file = {
name = filename,
size = stat.size,
mtime = stat.mtime,
-- FIXME: finer control client-side caching
headers = {
['Content-Type'] = get_type(filename),
['Content-Length'] = stat.size,
['Cache-Control'] = 'public, max-age=' .. (max_age / 1000),
['Last-Modified'] = date('!%c', stat.mtime),
['Etag'] = stat.size .. '-' .. stat.mtime
},
}
-- allocate cache entry
cache[filename] = file
-- should any changes in this file occur, invalidate cache entry
-- TODO: reuse caching technique from luvit/kernel
file.watch = uv.Watcher:new(filename)
-- TODO: watcher reports relative path: https://github.com/joyent/libuv/blob/master/src/unix/linux/inotify.c#L156-160
if false then
file.watch:on('change', invalidate_cache_entry)
else
file.watch:on('change', function ()
-- invalidate cache entry and free the watcher
if cache[filename] then
cache[filename] = nil
file.watch:close()
file.watch = nil
end
end)
end
-- shall we cache file contents?
local cache_it = is_cacheable(file)
serve(res, file, req.headers.range, cache_it)
end)
end
end
end
-- module
return setup