356 lines
10 KiB
Nim
356 lines
10 KiB
Nim
## Nim-Dagger
|
|
## Copyright (c) 2021 Status Research & Development GmbH
|
|
## Licensed under either of
|
|
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
## * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
## at your option.
|
|
## This file may not be copied, modified, or distributed except according to
|
|
## those terms.
|
|
|
|
import pkg/upraises
|
|
|
|
push: {.upraises: [].}
|
|
|
|
|
|
import std/sequtils
|
|
|
|
import pkg/questionable
|
|
import pkg/questionable/results
|
|
import pkg/chronicles
|
|
import pkg/chronos
|
|
import pkg/presto
|
|
import pkg/libp2p
|
|
import pkg/stew/base10
|
|
import pkg/confutils
|
|
|
|
import pkg/libp2p/routing_record
|
|
|
|
import ../node
|
|
import ../blocktype
|
|
import ../conf
|
|
import ../contracts
|
|
|
|
import ./json
|
|
|
|
proc validate(
|
|
pattern: string,
|
|
value: string): int
|
|
{.gcsafe, raises: [Defect].} =
|
|
0
|
|
|
|
proc encodeString(cid: type Cid): Result[string, cstring] =
|
|
ok($cid)
|
|
|
|
proc decodeString(T: type Cid, value: string): Result[Cid, cstring] =
|
|
Cid
|
|
.init(value)
|
|
.mapErr do(e: CidError) -> cstring:
|
|
case e
|
|
of CidError.Incorrect: "Incorrect Cid"
|
|
of CidError.Unsupported: "Unsupported Cid"
|
|
of CidError.Overrun: "Overrun Cid"
|
|
else: "Error parsing Cid"
|
|
|
|
proc encodeString(peerId: PeerID): Result[string, cstring] =
|
|
ok($peerId)
|
|
|
|
proc decodeString(T: type PeerID, value: string): Result[PeerID, cstring] =
|
|
PeerID.init(value)
|
|
|
|
proc encodeString(address: MultiAddress): Result[string, cstring] =
|
|
ok($address)
|
|
|
|
proc decodeString(T: type MultiAddress, value: string): Result[MultiAddress, cstring] =
|
|
MultiAddress
|
|
.init(value)
|
|
.mapErr do(e: string) -> cstring: cstring(e)
|
|
|
|
proc decodeString(T: type SomeUnsignedInt, value: string): Result[T, cstring] =
|
|
Base10.decode(T, value)
|
|
|
|
proc encodeString(value: SomeUnsignedInt): Result[string, cstring] =
|
|
ok(Base10.toString(value))
|
|
|
|
proc decodeString(T: type Duration, value: string): Result[T, cstring] =
|
|
let v = ? Base10.decode(uint32, value)
|
|
ok(v.minutes)
|
|
|
|
proc encodeString(value: Duration): Result[string, cstring] =
|
|
ok($value)
|
|
|
|
proc decodeString(T: type bool, value: string): Result[T, cstring] =
|
|
try:
|
|
ok(value.parseBool())
|
|
except CatchableError as exc:
|
|
let s: cstring = exc.msg
|
|
err(s) # err(exc.msg) won't compile
|
|
|
|
proc encodeString(value: bool): Result[string, cstring] =
|
|
ok($value)
|
|
|
|
proc decodeString(_: type UInt256, value: string): Result[UInt256, cstring] =
|
|
try:
|
|
ok UInt256.fromHex(value)
|
|
except ValueError as e:
|
|
err e.msg.cstring
|
|
|
|
proc initRestApi*(node: DaggerNodeRef, conf: DaggerConf): RestRouter =
|
|
var router = RestRouter.init(validate)
|
|
router.api(
|
|
MethodGet,
|
|
"/api/dagger/v1/connect/{peerId}") do (
|
|
peerId: PeerID,
|
|
addrs: seq[MultiAddress]) -> RestApiResponse:
|
|
## Connect to a peer
|
|
##
|
|
## If `addrs` param is supplied, it will be used to
|
|
## dial the peer, otherwise the `peerId` is used
|
|
## to invoke peer discovery, if it succeeds
|
|
## the returned addresses will be used to dial
|
|
##
|
|
|
|
if peerId.isErr:
|
|
return RestApiResponse.error(
|
|
Http400,
|
|
$peerId.error())
|
|
|
|
let addresses = if addrs.isOk and addrs.get().len > 0:
|
|
addrs.get()
|
|
else:
|
|
without peerRecord =? (await node.findPeer(peerId.get())):
|
|
return RestApiResponse.error(
|
|
Http400,
|
|
"Unable to find Peer!")
|
|
peerRecord.addresses.mapIt(it.address)
|
|
try:
|
|
await node.connect(peerId.get(), addresses)
|
|
return RestApiResponse.response("Successfully connected to peer")
|
|
except DialFailedError as e:
|
|
return RestApiResponse.error(Http400, "Unable to dial peer")
|
|
except CatchableError as e:
|
|
return RestApiResponse.error(Http400, "Unknown error dialling peer")
|
|
|
|
router.api(
|
|
MethodGet,
|
|
"/api/dagger/v1/download/{id}") do (
|
|
id: Cid, resp: HttpResponseRef) -> RestApiResponse:
|
|
## Download a file from the node in a streaming
|
|
## manner
|
|
##
|
|
|
|
if id.isErr:
|
|
return RestApiResponse.error(
|
|
Http400,
|
|
$id.error())
|
|
|
|
var
|
|
stream: LPStream
|
|
|
|
var bytes = 0
|
|
try:
|
|
without stream =? (await node.retrieve(id.get())), error:
|
|
return RestApiResponse.error(Http404, error.msg)
|
|
|
|
resp.addHeader("Content-Type", "application/octet-stream")
|
|
await resp.prepareChunked()
|
|
|
|
while not stream.atEof:
|
|
var
|
|
buff = newSeqUninitialized[byte](BlockSize)
|
|
len = await stream.readOnce(addr buff[0], buff.len)
|
|
|
|
buff.setLen(len)
|
|
if buff.len <= 0:
|
|
break
|
|
|
|
bytes += buff.len
|
|
trace "Sending chunk", size = buff.len
|
|
await resp.sendChunk(addr buff[0], buff.len)
|
|
await resp.finish()
|
|
except CatchableError as exc:
|
|
trace "Excepting streaming blocks", exc = exc.msg
|
|
return RestApiResponse.error(Http500)
|
|
finally:
|
|
trace "Sent bytes", cid = id.get(), bytes
|
|
if not stream.isNil:
|
|
await stream.close()
|
|
|
|
router.api(
|
|
MethodPost,
|
|
"/api/dagger/v1/storage/request/{cid}") do (
|
|
cid: Cid,
|
|
ppb: Option[uint],
|
|
duration: Option[Duration],
|
|
nodes: Option[uint],
|
|
loss: Option[uint],
|
|
renew: Option[bool]) -> RestApiResponse:
|
|
## Create a request for storage
|
|
##
|
|
## Cid - the cid of the previously uploaded dataset
|
|
## ppb - the price per byte the client is willing to pay
|
|
## duration - the duration of the contract
|
|
## nodeCount - the total amount of the nodes storing the dataset, including `lossTolerance`
|
|
## lossTolerance - the number of nodes losses the user is willing to tolerate
|
|
## autoRenew - should the contract be autorenewed -
|
|
## will fail unless the user has enough funds lockedup
|
|
##
|
|
|
|
var
|
|
cid =
|
|
if cid.isErr:
|
|
return RestApiResponse.error(Http400, $cid.error())
|
|
else:
|
|
cid.get()
|
|
|
|
ppb =
|
|
if ppb.isNone:
|
|
return RestApiResponse.error(Http400, "Missing ppb")
|
|
else:
|
|
if ppb.get().isErr:
|
|
return RestApiResponse.error(Http500, $ppb.get().error)
|
|
else:
|
|
ppb.get().get()
|
|
|
|
duration =
|
|
if duration.isNone:
|
|
return RestApiResponse.error(Http400, "Missing duration")
|
|
else:
|
|
if duration.get().isErr:
|
|
return RestApiResponse.error(Http500, $duration.get().error)
|
|
else:
|
|
duration.get().get()
|
|
|
|
nodes =
|
|
if nodes.isNone:
|
|
return RestApiResponse.error(Http400, "Missing node count")
|
|
else:
|
|
if nodes.get().isErr:
|
|
return RestApiResponse.error(Http500, $nodes.get().error)
|
|
else:
|
|
nodes.get().get()
|
|
|
|
loss =
|
|
if loss.isNone:
|
|
return RestApiResponse.error(Http400, "Missing loss tolerance")
|
|
else:
|
|
if loss.get().isErr:
|
|
return RestApiResponse.error(Http500, $loss.get().error)
|
|
else:
|
|
loss.get().get()
|
|
|
|
renew = if renew.isNone:
|
|
false
|
|
else:
|
|
if renew.get().isErr:
|
|
return RestApiResponse.error(Http500, $renew.get().error)
|
|
else:
|
|
renew.get().get()
|
|
|
|
try:
|
|
without storageCid =? (await node.requestStorage(
|
|
cid,
|
|
ppb,
|
|
duration,
|
|
nodes,
|
|
loss,
|
|
renew)), error:
|
|
return RestApiResponse.error(Http500, error.msg)
|
|
|
|
return RestApiResponse.response($storageCid)
|
|
except CatchableError as exc:
|
|
return RestApiResponse.error(Http500, exc.msg)
|
|
|
|
router.rawApi(
|
|
MethodPost,
|
|
"/api/dagger/v1/upload") do (
|
|
) -> RestApiResponse:
|
|
## Upload a file in a streamming manner
|
|
##
|
|
|
|
trace "Handling file upload"
|
|
var bodyReader = request.getBodyReader()
|
|
if bodyReader.isErr():
|
|
return RestApiResponse.error(Http500)
|
|
|
|
# Attempt to handle `Expect` header
|
|
# some clients (curl), wait 1000ms
|
|
# before giving up
|
|
#
|
|
await request.handleExpect()
|
|
|
|
let
|
|
reader = bodyReader.get()
|
|
stream = BufferStream.new()
|
|
storeFut = node.store(stream)
|
|
|
|
var bytes = 0
|
|
try:
|
|
while not reader.atEof:
|
|
var
|
|
buff = newSeqUninitialized[byte](BlockSize)
|
|
len = await reader.readOnce(addr buff[0], buff.len)
|
|
|
|
buff.setLen(len)
|
|
if len <= 0:
|
|
break
|
|
|
|
trace "Got chunk from endpoint", len = buff.len
|
|
await stream.pushData(buff)
|
|
bytes += len
|
|
|
|
await stream.pushEof()
|
|
without cid =? (await storeFut), error:
|
|
return RestApiResponse.error(Http500, error.msg)
|
|
|
|
trace "Uploaded file", bytes, cid = $cid
|
|
return RestApiResponse.response($cid)
|
|
except CancelledError as exc:
|
|
await reader.closeWait()
|
|
return RestApiResponse.error(Http500)
|
|
except AsyncStreamError:
|
|
await reader.closeWait()
|
|
return RestApiResponse.error(Http500)
|
|
finally:
|
|
await stream.close()
|
|
await reader.closeWait()
|
|
|
|
# if we got here something went wrong?
|
|
return RestApiResponse.error(Http500)
|
|
|
|
router.api(
|
|
MethodGet,
|
|
"/api/dagger/v1/info") do () -> RestApiResponse:
|
|
## Print rudimentary node information
|
|
##
|
|
|
|
var addrs: string
|
|
for a in node.switch.peerInfo.addrs:
|
|
addrs &= "- " & $a & "\n"
|
|
|
|
return RestApiResponse.response(
|
|
"Id: " & $node.switch.peerInfo.peerId &
|
|
"\nAddrs: \n" & addrs &
|
|
"\nRoot Dir: " & $conf.dataDir)
|
|
|
|
router.rawApi(
|
|
MethodPost,
|
|
"/api/dagger/v1/sales/availability") do () -> RestApiResponse:
|
|
## Add available storage to sell
|
|
##
|
|
## size - size of available storage in bytes
|
|
## duration - maximum time the storage should be sold for (in seconds)
|
|
## minPrice - minimum price to be paid (in amount of tokens)
|
|
|
|
let body = await request.getBody()
|
|
|
|
without availability =? Availability.fromJson(body), error:
|
|
return RestApiResponse.error(Http400, error.msg)
|
|
|
|
without contracts =? node.contracts:
|
|
return RestApiResponse.error(Http503, "Sales unavailable")
|
|
|
|
contracts.sales.add(availability)
|
|
return RestApiResponse.response(availability.id.toHex)
|
|
|
|
return router
|