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
|
||||
*.code-workspace
|
||||
*.exe
|
||||
app.js
|
||||
app.html
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ license = "MIT"
|
|||
# Dependencies
|
||||
|
||||
requires "nim >= 0.16.1"
|
||||
|
||||
requires "ws"
|
||||
requires "dotenv"
|
||||
skipDirs = @["examples", "experiments", "tests"]
|
||||
|
||||
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
|
||||
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">
|
||||
"""
|
||||
html = """
|
||||
|
||||
const html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -23,33 +18,79 @@ const
|
|||
<title>$1</title>
|
||||
$2
|
||||
</head>
|
||||
<body id="body">
|
||||
<div id="ROOT" />
|
||||
<script type="text/javascript" src="$1.js"></script>
|
||||
<body id="body" class="site">
|
||||
<div id="ROOT"></div>
|
||||
<script type="text/javascript" src="/app.js"></script>
|
||||
$3
|
||||
</body>
|
||||
</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) =
|
||||
if os.execShellCmd(cmd) != 0:
|
||||
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...")
|
||||
exec("nim js --out:" & name & ".js " & rest)
|
||||
let dest = name & ".html"
|
||||
writeFile(dest, html % [name, selectedCss])
|
||||
if run: openDefaultBrowser(dest)
|
||||
let cmd = "nim js --out:" & "app" & ".js " & rest
|
||||
if watch:
|
||||
discard os.execShellCmd(cmd)
|
||||
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 =
|
||||
var op = initOptParser()
|
||||
var rest = op.cmdLineRest
|
||||
var file = ""
|
||||
var run = false
|
||||
var selectedCss = ""
|
||||
var watch = false
|
||||
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
||||
|
||||
var selectedCss = ""
|
||||
while true:
|
||||
op.next()
|
||||
case op.kind
|
||||
|
|
@ -59,8 +100,11 @@ proc main =
|
|||
run = true
|
||||
rest = rest.replace("--run ")
|
||||
of "css":
|
||||
if op.val != "":
|
||||
selectedCss = readFile(op.val)
|
||||
else:
|
||||
selectedCss = css
|
||||
rest = rest.replace("--css ")
|
||||
rest = rest.substr(rest.find(" "))
|
||||
else: discard
|
||||
of cmdShortOption:
|
||||
if op.key == "r":
|
||||
|
|
@ -73,25 +117,11 @@ proc main =
|
|||
of cmdEnd: break
|
||||
|
||||
if file.len == 0: quit "filename expected"
|
||||
let name = file.splitFile.name
|
||||
build(name, rest, selectedCss, run)
|
||||
if run:
|
||||
spawn serve()
|
||||
if watch:
|
||||
# TODO: launch http server
|
||||
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(name, rest, selectedCss, run)
|
||||
files[path] = getLastModificationTime(path)
|
||||
else:
|
||||
files[path] = getLastModificationTime(path)
|
||||
spawn watchBuild(file, selectedCss, rest)
|
||||
build(rest,selectedCss, run, watch)
|
||||
sync()
|
||||
|
||||
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