add static server,websocket refresh watching mode (#149)
* add static server,websocket refresh watching mode * watch optional * we dont quit when building error in watch mode * staticDir,publicUrl configure through dot env * remove unused import * serve optional
This commit is contained in:
parent
22e2e226cb
commit
98a2d1ce50
5 changed files with 248 additions and 44 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,3 +3,5 @@ nimcache/
|
||||||
karax/tools/karun
|
karax/tools/karun
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
*.exe
|
*.exe
|
||||||
|
app.js
|
||||||
|
app.html
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ license = "MIT"
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
requires "nim >= 0.16.1"
|
requires "nim >= 0.16.1"
|
||||||
|
requires "ws"
|
||||||
|
requires "dotenv"
|
||||||
skipDirs = @["examples", "experiments", "tests"]
|
skipDirs = @["examples", "experiments", "tests"]
|
||||||
|
|
||||||
bin = @["karax/tools/karun"]
|
bin = @["karax/tools/karun"]
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,16 @@
|
||||||
## Simple tool to quickly run Karax applications. Generates the HTML
|
|
||||||
## required to run a Karax app and opens it in a browser.
|
|
||||||
|
|
||||||
import os,
|
|
||||||
strutils,
|
|
||||||
parseopt,
|
|
||||||
browsers,
|
|
||||||
times,
|
|
||||||
tables
|
|
||||||
|
|
||||||
|
|
||||||
|
import os, strutils, browsers,times, tables
|
||||||
|
import parseopt
|
||||||
|
import threadpool
|
||||||
|
import static_server
|
||||||
|
|
||||||
const
|
const
|
||||||
css = """
|
css = """
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
"""
|
"""
|
||||||
html = """
|
|
||||||
|
const html = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -23,33 +18,79 @@ const
|
||||||
<title>$1</title>
|
<title>$1</title>
|
||||||
$2
|
$2
|
||||||
</head>
|
</head>
|
||||||
<body id="body">
|
<body id="body" class="site">
|
||||||
<div id="ROOT" />
|
<div id="ROOT"></div>
|
||||||
<script type="text/javascript" src="$1.js"></script>
|
<script type="text/javascript" src="/app.js"></script>
|
||||||
|
$3
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
const websocket = """
|
||||||
|
<script type="text/javascript">
|
||||||
|
var ws = new WebSocket("ws://localhost:8080/ws");
|
||||||
|
|
||||||
|
ws.onopen = function(evt) {
|
||||||
|
console.log("Connection open ...");
|
||||||
|
ws.send("Hello WebSockets!");
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(evt) {
|
||||||
|
console.log( "Received Message: " + evt.data);
|
||||||
|
if(evt.data == "refresh"){
|
||||||
|
window.location.href = window.location.href
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function(evt) {
|
||||||
|
console.log("Connection closed.");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
proc exec(cmd: string) =
|
proc exec(cmd: string) =
|
||||||
if os.execShellCmd(cmd) != 0:
|
if os.execShellCmd(cmd) != 0:
|
||||||
quit "External command failed: " & cmd
|
quit "External command failed: " & cmd
|
||||||
|
|
||||||
proc build(name: string, rest: string, selectedCss: string, run: bool) =
|
proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
|
||||||
echo("Building...")
|
echo("Building...")
|
||||||
exec("nim js --out:" & name & ".js " & rest)
|
let cmd = "nim js --out:" & "app" & ".js " & rest
|
||||||
let dest = name & ".html"
|
if watch:
|
||||||
writeFile(dest, html % [name, selectedCss])
|
discard os.execShellCmd(cmd)
|
||||||
if run: openDefaultBrowser(dest)
|
else:
|
||||||
|
exec cmd
|
||||||
|
let dest = "app" & ".html"
|
||||||
|
let script = if run and watch: websocket else: ""
|
||||||
|
writeFile(dest, html % ["app", selectedCss, script])
|
||||||
|
if run: openDefaultBrowser("http://localhost:8080")
|
||||||
|
|
||||||
|
proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} =
|
||||||
|
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
||||||
|
while true:
|
||||||
|
sleep(300)
|
||||||
|
for path in walkDirRec("."):
|
||||||
|
if ".git" in path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if files.hasKey(path):
|
||||||
|
if files[path] != getLastModificationTime(path):
|
||||||
|
echo("File changed: " & path)
|
||||||
|
build(rest,selectedCss, false, true)
|
||||||
|
files[path] = getLastModificationTime(path)
|
||||||
|
else:
|
||||||
|
if absolutePath(path) in [absolutePath("app" & ".js"),absolutePath("app" & ".html")]:
|
||||||
|
continue
|
||||||
|
files[path] = getLastModificationTime(path)
|
||||||
|
|
||||||
|
proc serve(){.thread.} =
|
||||||
|
serveStatic()
|
||||||
|
|
||||||
proc main =
|
proc main =
|
||||||
var op = initOptParser()
|
var op = initOptParser()
|
||||||
var rest = op.cmdLineRest
|
var rest = op.cmdLineRest
|
||||||
var file = ""
|
var file = ""
|
||||||
var run = false
|
var run = false
|
||||||
var selectedCss = ""
|
|
||||||
var watch = false
|
var watch = false
|
||||||
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
var selectedCss = ""
|
||||||
|
|
||||||
while true:
|
while true:
|
||||||
op.next()
|
op.next()
|
||||||
case op.kind
|
case op.kind
|
||||||
|
|
@ -59,8 +100,11 @@ proc main =
|
||||||
run = true
|
run = true
|
||||||
rest = rest.replace("--run ")
|
rest = rest.replace("--run ")
|
||||||
of "css":
|
of "css":
|
||||||
selectedCss = css
|
if op.val != "":
|
||||||
rest = rest.replace("--css ")
|
selectedCss = readFile(op.val)
|
||||||
|
else:
|
||||||
|
selectedCss = css
|
||||||
|
rest = rest.substr(rest.find(" "))
|
||||||
else: discard
|
else: discard
|
||||||
of cmdShortOption:
|
of cmdShortOption:
|
||||||
if op.key == "r":
|
if op.key == "r":
|
||||||
|
|
@ -73,25 +117,11 @@ proc main =
|
||||||
of cmdEnd: break
|
of cmdEnd: break
|
||||||
|
|
||||||
if file.len == 0: quit "filename expected"
|
if file.len == 0: quit "filename expected"
|
||||||
let name = file.splitFile.name
|
if run:
|
||||||
build(name, rest, selectedCss, run)
|
spawn serve()
|
||||||
if watch:
|
if watch:
|
||||||
# TODO: launch http server
|
spawn watchBuild(file, selectedCss, rest)
|
||||||
while true:
|
build(rest,selectedCss, run, watch)
|
||||||
sleep(300)
|
sync()
|
||||||
for path in walkDirRec("."):
|
|
||||||
if ".git" in path:
|
|
||||||
continue
|
|
||||||
if files.hasKey(path):
|
|
||||||
if files[path] != getLastModificationTime(path):
|
|
||||||
echo("File changed: " & path)
|
|
||||||
build(name, rest, selectedCss, run)
|
|
||||||
files[path] = getLastModificationTime(path)
|
|
||||||
else:
|
|
||||||
files[path] = getLastModificationTime(path)
|
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
1
karax/tools/karun.nims
Normal file
1
karax/tools/karun.nims
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
switch("threads", "on")
|
||||||
170
karax/tools/static_server.nim
Normal file
170
karax/tools/static_server.nim
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
|
||||||
|
logging, httpcore, asyncfile, options
|
||||||
|
import asynchttpserver
|
||||||
|
from cgi import decodeUrl
|
||||||
|
import ws,tables,times
|
||||||
|
import dotenv
|
||||||
|
|
||||||
|
type
|
||||||
|
RawHeaders* = seq[tuple[key, val: string]]
|
||||||
|
|
||||||
|
proc toStr(headers: Option[RawHeaders]): string =
|
||||||
|
return $newHttpHeaders(headers.get(@({:})))
|
||||||
|
|
||||||
|
proc send(
|
||||||
|
request: Request, code: HttpCode, headers: Option[RawHeaders], body: string
|
||||||
|
): Future[void] =
|
||||||
|
return request.respond(
|
||||||
|
code, body, newHttpHeaders(headers.get(@({:})))
|
||||||
|
)
|
||||||
|
|
||||||
|
proc statusContent(request: Request, status: HttpCode, content: string,
|
||||||
|
headers: Option[RawHeaders]): Future[void] =
|
||||||
|
try:
|
||||||
|
result = send(request, status, headers, content)
|
||||||
|
when not defined(release):
|
||||||
|
logging.debug(" $1 $2" % [$status, toStr(headers)])
|
||||||
|
except:
|
||||||
|
logging.error("Could not send response: $1" % osErrorMsg(osLastError()))
|
||||||
|
|
||||||
|
proc sendStaticIfExists(
|
||||||
|
req: Request, paths: seq[string]
|
||||||
|
): Future[HttpCode] {.async.} =
|
||||||
|
result = Http200
|
||||||
|
let mimes = newMimetypes()
|
||||||
|
for p in paths:
|
||||||
|
if existsFile(p):
|
||||||
|
|
||||||
|
var fp = getFilePermissions(p)
|
||||||
|
if not fp.contains(fpOthersRead):
|
||||||
|
return Http403
|
||||||
|
|
||||||
|
let fileSize = getFileSize(p)
|
||||||
|
let ext = p.splitFile.ext
|
||||||
|
let mimetype = mimes.getMimetype(
|
||||||
|
if ext.len > 0: ext[1 .. ^1]
|
||||||
|
else: ""
|
||||||
|
)
|
||||||
|
if fileSize < 10_000_000: # 10 mb
|
||||||
|
var file = readFile(p)
|
||||||
|
|
||||||
|
var hashed = getMD5(file)
|
||||||
|
|
||||||
|
# If the user has a cached version of this file and it matches our
|
||||||
|
# version, let them use it
|
||||||
|
if req.headers.hasKey("If-None-Match") and req.headers["If-None-Match"] == hashed:
|
||||||
|
await req.statusContent(Http304, "", none[RawHeaders]())
|
||||||
|
else:
|
||||||
|
await req.statusContent(Http200, file, some(@({
|
||||||
|
"Content-Type": mimetype,
|
||||||
|
"ETag": hashed
|
||||||
|
})))
|
||||||
|
else:
|
||||||
|
let headers = @({
|
||||||
|
"Content-Type": mimetype,
|
||||||
|
"Content-Length": $fileSize
|
||||||
|
})
|
||||||
|
await req.statusContent(Http200, "", some(headers))
|
||||||
|
|
||||||
|
var fileStream = newFutureStream[string]("sendStaticIfExists")
|
||||||
|
var file = openAsync(p, fmRead)
|
||||||
|
# Let `readToStream` write file data into fileStream in the
|
||||||
|
# background.
|
||||||
|
asyncCheck file.readToStream(fileStream)
|
||||||
|
# The `writeFromStream` proc will complete once all the data in the
|
||||||
|
# `bodyStream` has been written to the file.
|
||||||
|
while true:
|
||||||
|
let (hasValue, value) = await fileStream.read()
|
||||||
|
if hasValue:
|
||||||
|
await req.client.send(value)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we get to here then no match could be found.
|
||||||
|
return Http404
|
||||||
|
|
||||||
|
|
||||||
|
proc handleFileRequest(
|
||||||
|
req: Request
|
||||||
|
): Future[HttpCode] {.async.} =
|
||||||
|
# Find static file.
|
||||||
|
var reqPath = cgi.decodeUrl(req.url.path)
|
||||||
|
var publicUrl = getEnv("publicUrl")
|
||||||
|
var staticDir = getEnv("staticDir")
|
||||||
|
if not publicUrl.endsWith("/"):
|
||||||
|
publicUrl = publicUrl & "/"
|
||||||
|
reqPath = reqPath.substr(publicUrl.len)
|
||||||
|
|
||||||
|
let path = normalizedPath(
|
||||||
|
staticDir / reqPath
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify that this isn't outside our static dir.
|
||||||
|
var status = Http400
|
||||||
|
let pathDir = path.splitFile.dir / ""
|
||||||
|
|
||||||
|
if pathDir.startsWith(publicUrl):
|
||||||
|
if existsDir(path):
|
||||||
|
status = await sendStaticIfExists(
|
||||||
|
req,
|
||||||
|
@[path / "index.html", path / "index.htm"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
status = await sendStaticIfExists(req, @[path])
|
||||||
|
return status
|
||||||
|
|
||||||
|
proc handleWs(req: Request) {.async.} =
|
||||||
|
var ws = await newWebSocket(req)
|
||||||
|
await ws.send("Welcome to simple echo server")
|
||||||
|
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
||||||
|
let watchedFiles = [absolutePath "app.js",absolutePath "app.html"]
|
||||||
|
for path in watchedFiles:
|
||||||
|
files[path] = getLastModificationTime(path)
|
||||||
|
while ws.readyState == Open:
|
||||||
|
await sleepAsync(500)
|
||||||
|
var changed = false
|
||||||
|
for path in watchedFiles:
|
||||||
|
if files[path] != getLastModificationTime(path):
|
||||||
|
changed = true
|
||||||
|
files[path] = getLastModificationTime(path)
|
||||||
|
if changed:
|
||||||
|
await ws.send("refresh")
|
||||||
|
changed = false
|
||||||
|
|
||||||
|
|
||||||
|
proc serveStatic*() =
|
||||||
|
if fileExists( "static.env" ):
|
||||||
|
var env:DotEnv
|
||||||
|
env = initDotEnv(getCurrentDir(), "static.env")
|
||||||
|
env.overload()
|
||||||
|
else:
|
||||||
|
loadEnvFromString("""
|
||||||
|
staticDir="./src/assets/"
|
||||||
|
publicUrl="public"
|
||||||
|
""")
|
||||||
|
|
||||||
|
var server = newAsyncHttpServer()
|
||||||
|
proc cb(req: Request) {.gcsafe, async.} =
|
||||||
|
if req.url.path == "/ws":
|
||||||
|
await handleWs(req)
|
||||||
|
if req.url.path == "/":
|
||||||
|
await req.respond(Http200, readFile "app.html" )
|
||||||
|
elif req.url.path == "/app.js":
|
||||||
|
let file = absolutePath("app" & ".js")
|
||||||
|
if not file.fileExists:
|
||||||
|
logging.error("$1 not exists!" % file )
|
||||||
|
if fpUserRead notin os.getFilePermissions(file):
|
||||||
|
logging.error("Could not read $1!" % file )
|
||||||
|
await req.respond(Http200, readFile( file ))
|
||||||
|
else:
|
||||||
|
let status = await handleFileRequest(req)
|
||||||
|
if status != Http200:
|
||||||
|
await req.respond(status,"")
|
||||||
|
|
||||||
|
waitFor server.serve(Port(8080), cb)
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
serveStatic()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue