@@ -80,8 +80,7 @@ Return the filesystem path corresponding to a requested path, or an empty
80
80
String if the file was not found.
81
81
"""
82
82
function get_fs_path (req_path:: AbstractString ):: String
83
- uri = HTTP. URI (req_path)
84
- # first element after the split is **always** "/" --> 2:end
83
+ uri = HTTP. URI (req_path)
85
84
r_parts = HTTP. URIs. unescapeuri .(split (lstrip (uri. path, ' /' ), ' /' ))
86
85
fs_path = joinpath (r_parts... )
87
86
@@ -95,7 +94,8 @@ function get_fs_path(req_path::AbstractString)::String
95
94
isfile (tmp) && return tmp
96
95
97
96
# content of the dir will be shown
98
- isdir (fs_path) && return fs_path
97
+ # we ensure there's a slash at the end (see issue #135)
98
+ isdir (fs_path) && return joinpath (fs_path, " " )
99
99
100
100
# 404 will be shown
101
101
return " "
@@ -109,7 +109,7 @@ Append `/` to the path part of `url`; i.e., transform `a/b` to `a/b/` and `/a/b?
109
109
"""
110
110
function append_slash (url_str:: AbstractString )
111
111
uri = HTTP. URI (url_str)
112
- return string (endswith (uri. path, " /" ) ? uri : merge (uri; path = uri. path * " /" ))
112
+ return string (endswith (uri. path, " /" ) ? uri : HTTP . merge (uri; path = uri. path * " /" ))
113
113
end
114
114
115
115
"""
@@ -140,16 +140,18 @@ function get_dir_list(dir::AbstractString)
140
140
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/spcss">
141
141
<title>Directory listing</title>
142
142
<style>
143
- a {text-decoration: none;}
143
+ a {text-decoration: none;}
144
144
</style>
145
145
</head>
146
146
<body>
147
147
<h1 style='margin-top: 1em;'>
148
148
Directory listing
149
149
</h1>
150
150
<h3>
151
- <a href="/" alt="root">🏠</a> <a href="/$(dirname (dir)) " alt="parent dir">⬆️</a> path: <code style='color:gray;'>$(sdir) </code>
152
- </h2>
151
+ <a href="/" alt="root">🏠</a>
152
+ <a href="/$(dirname (dir)) " alt="parent dir">⬆️</a>
153
+ path: <code style='color:gray;'>$(sdir) </code>
154
+ </h3>
153
155
154
156
<hr>
155
157
<ul>
@@ -160,19 +162,21 @@ function get_dir_list(dir::AbstractString)
160
162
list_dirs = [d for d in list if d ∉ list_files]
161
163
162
164
for fname in list_files
163
- link = lstrip_cdir (fname)
164
- name = splitdir (fname)[end ]
165
- post = ifelse (islink (fname), " @" , " " )
165
+ link = lstrip_cdir (fname)
166
+ name = splitdir (fname)[end ]
167
+ post = ifelse (islink (fname), " @" , " " )
166
168
write (io, """
167
169
<li><a href="/$(link) ">$(name)$(post) </a></li>
168
170
"""
169
171
)
170
172
end
171
173
for fdir in list_dirs
172
- link = lstrip_cdir (fdir)
173
- name = splitdir (fdir)[end ]
174
- pre = " 📂 "
175
- post = ifelse (islink (fdir), " @" , " " )
174
+ link = lstrip_cdir (fdir)
175
+ # ensure ends with slash, see #135
176
+ link *= ifelse (endswith (link, " /" ), " " , " /" )
177
+ name = splitdir (fdir)[end ]
178
+ pre = " 📂 "
179
+ post = ifelse (islink (fdir), " @" , " " )
176
180
write (io, """
177
181
<li><a href="/$(link) ">$(pre)$(name)$(post) </a></li>
178
182
"""
@@ -197,14 +201,15 @@ to be watched can be added, and a request (e.g. a path entered in a tab of the
197
201
browser), and converts it to the appropriate file system path.
198
202
199
203
The cases are as follows:
200
- 1. the path corresponds exactly to a file. If it's a html-like file,
201
- LiveServer will try injecting the reloading `<script>` (see file
202
- `client.html`) at the end, just before the `</body>` tag.
203
- 2. the path corresponds to a directory in which there is an `index.html`,
204
- same action as (1) assuming the `index.html` is implicit.
205
- 3. the path corresponds to a directory in which there is not an `index.html`,
206
- list the directory contents.
207
- 4. not (1,2,3), a 404 is served.
204
+ 1. FILE: the path corresponds exactly to a file. If it's a html-like file,
205
+ LiveServer will try injecting the reloading `<script>` (see file
206
+ `client.html`) at the end, just before the `</body>` tag. Otherwise
207
+ we let the browser attempt to show it (e.g. if it's an image).
208
+ 2. WEB-DIRECTORY: the path corresponds to a directory in which there is an
209
+ `index.html`, same action as (1) assuming the `index.html` is implicit.
210
+ 3. PLAIN-DIRECTORY: the path corresponds to a directory in which there is not
211
+ an `index.html`, list the directory contents.
212
+ 4. 404: not (1,2,3), a 404 is served.
208
213
209
214
All files served are added to the file watcher, which is responsible to check
210
215
whether they're already watched or not. Finally the file is served via a 200
@@ -221,9 +226,8 @@ function serve_file(
221
226
fs_path = get_fs_path (req. target)
222
227
223
228
# if get_fs_path returns an empty string, there's two cases:
224
- # 1. the path is a directory without an `index.html` --> list dir
225
- # 2. otherwise serve a 404 (see if there's a dedicated 404 path,
226
- # otherwise just use a basic one).
229
+ # 1. [CASE 3] the path is a directory without an `index.html` --> list dir
230
+ # 2. [CASE 4] otherwise serve a 404 (see if there's a dedicated 404 path,
227
231
if isempty (fs_path)
228
232
229
233
if req. target == " /"
@@ -251,14 +255,15 @@ function serve_file(
251
255
end
252
256
end
253
257
258
+ # [CASE 2]
254
259
if isdir (fs_path)
255
260
index_page = get_dir_list (fs_path)
256
261
return HTTP. Response (200 , index_page)
257
262
end
258
263
259
264
# In what follows, fs_path points to a file
260
- # --> html-like: try to inject reload-script
261
- # --> other: just get the browser to show it
265
+ # --> [CASE 1a] html-like: try to inject reload-script
266
+ # --> [CASE 1b] other: just get the browser to show it
262
267
#
263
268
ext = lstrip (last (splitext (fs_path)), ' .' ) |> string
264
269
content = read (fs_path, String)
@@ -391,59 +396,87 @@ end
391
396
392
397
393
398
"""
394
- serve(filewatcher; host="127.0.0.1", port=8000, dir="", verbose=false, coreloopfun=(c,fw)->nothing, inject_browser_reload_script::Bool = true, launch_browser::Bool = false, allow_cors::Bool = false )
399
+ serve(filewatcher; ... )
395
400
396
401
Main function to start a server at `http://host:port` and render what is in the current
397
402
directory. (See also [`example`](@ref) for an example folder).
398
403
399
404
# Arguments
400
405
401
- - `filewatcher` is a file watcher implementing the API described for [`SimpleWatcher`](@ref) (which also is the default) and messaging the viewers (via WebSockets) upon detecting file changes.
402
- - `port` is an integer between 8000 (default) and 9000.
403
- - `dir` specifies where to launch the server if not the current working directory.
404
- - `verbose` is a boolean switch to make the server print information about file changes and connections.
405
- - `coreloopfun` specifies a function which can be run every 0.1 second while the liveserver is going; it takes two arguments: the cycle counter and the filewatcher. By default the coreloop does nothing.
406
- - `launch_browser=false` specifies whether to launch the ambient browser at the localhost URL or not.
407
- - `allow_cors::Bool=false` will allow cross origin (CORS) requests to access the server via the "Access-Control-Allow-Origin" header.
408
- - `preprocess_request=identity`: specifies a function which can transform a request before a response is returned; its only argument is the current request.
409
-
410
- # Example
411
-
412
- ```julia
413
- LiveServer.example()
414
- serve(host="127.0.0.1", port=8080, dir="example", verbose=true, launch_browser=true)
415
- ```
416
-
417
- You should then see the `index.html` page from the `example` folder being rendered. If you change the file, the browser will automatically reload the
418
- page and show the changes.
419
- """
420
- function serve (fw:: FileWatcher = SimpleWatcher (file_changed_callback);
421
- host:: String = " 127.0.0.1" , port:: Int = 8000 , dir:: AbstractString = " " , verbose:: Bool = false ,
422
- coreloopfun:: Function = (c, fw)-> nothing ,
423
- preprocess_request= identity,
424
- inject_browser_reload_script:: Bool = true ,
425
- launch_browser:: Bool = false ,
426
- allow_cors:: Bool = false )
427
-
428
- 8000 ≤ port ≤ 9000 || throw (ArgumentError (" The port must be between 8000 and 9000." ))
429
- setverbose (verbose)
430
-
431
- if ! isempty (dir)
432
- isdir (dir) || throw (ArgumentError (" The specified dir '$dir ' is not recognised." ))
433
- CONTENT_DIR[] = dir
434
- end
406
+ `filewatcher`: a file watcher implementing the API described for
407
+ [`SimpleWatcher`](@ref) (which also is the default) and
408
+ messaging the viewers (via WebSockets) upon detecting file
409
+ changes.
410
+ `port`: integer between 8000 (default) and 9000.
411
+ `dir`: string specifying where to launch the server if not the current
412
+ working directory.
413
+ `verbose`: boolean switch to make the server print information about file
414
+ changes and connections.
415
+ `coreloopfun`: function which can be run every 0.1 second while the
416
+ liveserver is running; it takes two arguments: the cycle
417
+ counter and the filewatcher. By default the coreloop does
418
+ nothing.
419
+ `launch_browser`: boolean specifying whether to launch the ambient browser
420
+ at the localhost or not (default: false).
421
+ `allow_cors`: boolean allowing cross origin (CORS) requests to access the
422
+ server via the "Access-Control-Allow-Origin" header.
423
+ `preprocess_request`: function specifying the transformation of a request
424
+ before it is returned; its only argument is the
425
+ current request.
426
+ # Example
427
+
428
+ ```julia
429
+ LiveServer.example()
430
+ serve(host="127.0.0.1", port=8080, dir="example", launch_browser=true)
431
+ ```
432
+
433
+ You should then see the `index.html` page from the `example` folder being
434
+ rendered. If you change the file, the browser will automatically reload the
435
+ page and show the changes.
436
+ """
437
+ function serve (
438
+ fw:: FileWatcher = SimpleWatcher (file_changed_callback);
439
+ # kwargs
440
+ host:: String = " 127.0.0.1" ,
441
+ port:: Int = 8000 ,
442
+ dir:: AbstractString = " " ,
443
+ verbose:: Bool = false ,
444
+ coreloopfun:: Function = (c, fw)-> nothing ,
445
+ preprocess_request:: Function = identity,
446
+ inject_browser_reload_script:: Bool = true ,
447
+ launch_browser:: Bool = false ,
448
+ allow_cors:: Bool = false
449
+ ):: Nothing
450
+
451
+ 8000 ≤ port ≤ 9000 || throw (
452
+ ArgumentError (" The port must be between 8000 and 9000." )
453
+ )
454
+ setverbose (verbose)
455
+
456
+ if ! isempty (dir)
457
+ isdir (dir) || throw (
458
+ ArgumentError (" The specified dir '$dir ' is not recognised." )
459
+ )
460
+ CONTENT_DIR[] = dir
461
+ end
435
462
436
463
start (fw)
437
464
438
465
# make request handler
439
466
req_handler = HTTP. RequestHandlerFunction () do req
440
467
req = preprocess_request (req)
441
- serve_file (fw, req; inject_browser_reload_script = inject_browser_reload_script, allow_cors = allow_cors)
468
+ serve_file (
469
+ fw, req;
470
+ inject_browser_reload_script = inject_browser_reload_script,
471
+ allow_cors = allow_cors
472
+ )
442
473
end
443
474
444
475
server = Sockets. listen (parse (IPAddr, host), port)
445
476
url = " http://$(host == string (Sockets. localhost) ? " localhost" : host) :$port "
446
- println (" ✓ LiveServer listening on $url / ...\n (use CTRL+C to shut down)" )
477
+ println (
478
+ " ✓ LiveServer listening on $url / ...\n (use CTRL+C to shut down)"
479
+ )
447
480
@async HTTP. listen (host, port;
448
481
server= server, readtimeout= 0 , reuse_limit= 0 ) do http:: HTTP.Stream
449
482
# reuse_limit=0 ensures that there won't be an error if killing and restarting the server.
0 commit comments