131 lines
4.1 KiB
Nim
131 lines
4.1 KiB
Nim
import std/asyncdispatch, std/asynchttpserver, std/parseutils, std/random, std/net, std/strutils, std/tables
|
|
import ../blobsets
|
|
|
|
export net.Port
|
|
|
|
type
|
|
HttpStoreServer = ref object
|
|
server: AsyncHttpServer
|
|
store: BlobStore
|
|
ingests: Table[string, IngestStream]
|
|
blobs: Table[BlobId, BlobStream]
|
|
# TODO: tables must be purged periodically
|
|
rng: Rand
|
|
|
|
proc newHttpStoreServer*(backend: BlobStore; seed = 1'i64): HttpStoreServer =
|
|
## Create a new HTTP server for a given store.
|
|
randomize()
|
|
HttpStoreServer(
|
|
server: newAsyncHttpServer(),
|
|
store: backend,
|
|
ingests: initTable[string, IngestStream](),
|
|
blobs: initTable[BlobId, BlobStream](),
|
|
rng: initRand(seed))
|
|
|
|
func parseRange(range: string): tuple[a: int, b: int] =
|
|
## Parse an HTTP byte range string.
|
|
var start = skip(range, "bytes=")
|
|
if start > 0:
|
|
start.inc parseInt(range, result.a, start)
|
|
if skipWhile(range, {'-'}, start) == 1:
|
|
discard parseInt(range, result.b, start+1)
|
|
if result.b < result.a:
|
|
reset result
|
|
|
|
proc blobStream(hss: HttpStoreServer; path: string): BlobStream =
|
|
let elems = path.split '/'
|
|
if not elems.len == 3:
|
|
raise newException(ValueError, "bad GET path " & path)
|
|
let
|
|
kind = case elems[1]
|
|
of $dataBlob: dataBlob
|
|
of $metaBlob: metaBlob
|
|
else: raise newException(ValueError, "bad GET path " & path)
|
|
blob = toBlobId elems[2]
|
|
if hss.blobs.contains blob:
|
|
result = hss.blobs[blob]
|
|
else:
|
|
result = hss.store.openBlobStream(blob, kind=kind)
|
|
hss.blobs[blob] = result
|
|
|
|
proc head(hss: HttpStoreServer; req: Request): Future[void] =
|
|
discard hss.blobStream(req.url.path)
|
|
# cache the stream in the blob table or raise an exception
|
|
req.respond(Http200, "")
|
|
|
|
proc get(hss: HttpStoreServer; req: Request) {.async.} =
|
|
let stream = hss.blobStream(req.url.path)
|
|
var
|
|
pos: BiggestInt
|
|
len = blobLeafSize
|
|
let range = req.headers.getOrDefault "range"
|
|
if range != "":
|
|
let
|
|
(startPos, endPos) = parseRange range
|
|
pos = startPos
|
|
len = endPos - startPos
|
|
stream.pos = pos
|
|
var body = newString(len)
|
|
len = await stream.read(body[0].addr, len)
|
|
body.setLen len
|
|
let headers = newHttpHeaders({"Range": "bytes=$1-$2" % [ $pos, $(pos+len) ]})
|
|
await req.respond(Http206, body, headers)
|
|
|
|
proc putIngest(hss: HttpStoreServer; req: Request) {.async.} =
|
|
let stream = hss.ingests[req.url.path]
|
|
await stream.ingest(req.body)
|
|
await req.respond(Http204, "")
|
|
|
|
proc postIngest(hss: HttpStoreServer; req: Request) {.async.} =
|
|
case req.url.path
|
|
of "/ingest/" & $dataBlob:
|
|
let
|
|
size = parseBiggestInt req.headers["ingest-size"]
|
|
key = "/ingest/" & $hss.rng.next
|
|
headers = newHttpHeaders({"Location": key})
|
|
hss.ingests[key] = hss.store.openIngestStream(size, dataBlob)
|
|
await req.respond(Http201, "", headers)
|
|
of "/ingest/" & $metaBlob:
|
|
let
|
|
size = parseBiggestInt req.headers["ingest-size"]
|
|
key = "/ingest/" & $hss.rng.next
|
|
headers = newHttpHeaders({"Location": key})
|
|
hss.ingests[key] = hss.store.openIngestStream(size, metaBlob)
|
|
await req.respond(Http201, "", headers)
|
|
else:
|
|
let
|
|
stream = hss.ingests[req.url.path]
|
|
(blob, size) = await finish stream
|
|
headers = newHttpHeaders({
|
|
"blob-id": blob.toHex,
|
|
"blob-size": $size,
|
|
})
|
|
hss.ingests.del req.url.path
|
|
await req.respond(Http204, "", headers)
|
|
|
|
proc serve*(hss: HttpStoreServer; port = Port(80)): Future[void] =
|
|
## Serve requests to HTTP store.
|
|
proc handleRequest(req: Request) {.async.} =
|
|
try:
|
|
let fut = case req.reqMethod
|
|
of HttpGET:
|
|
get(hss, req)
|
|
of HttpHEAD:
|
|
head(hss, req)
|
|
of HttpPUT:
|
|
putIngest(hss, req)
|
|
of HttpPOST:
|
|
postIngest(hss, req)
|
|
else:
|
|
req.respond(Http501, "method not implemented")
|
|
await fut
|
|
except KeyError:
|
|
await req.respond(Http404, "")
|
|
# there is a bug with responding to HEAD with a message
|
|
except ValueError:
|
|
await req.respond(Http400, getCurrentExceptionMsg())
|
|
except:
|
|
try: await req.respond(Http500, getCurrentExceptionMsg())
|
|
except: discard
|
|
hss.server.serve(port, handleRequest)
|