blobsets/src/blobsets/httpservers.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)