2022-05-19 19:56:03 +00:00
|
|
|
## Nim-Codex
|
2022-01-10 15:32:56 +00:00
|
|
|
## 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.
|
|
|
|
|
2022-03-18 22:17:51 +00:00
|
|
|
import pkg/upraises
|
|
|
|
|
|
|
|
push: {.upraises: [].}
|
|
|
|
|
2022-01-10 15:32:56 +00:00
|
|
|
|
|
|
|
import std/sequtils
|
|
|
|
|
|
|
|
import pkg/questionable
|
|
|
|
import pkg/questionable/results
|
|
|
|
import pkg/chronicles
|
|
|
|
import pkg/chronos
|
|
|
|
import pkg/presto
|
|
|
|
import pkg/libp2p
|
2022-04-06 00:34:29 +00:00
|
|
|
import pkg/stew/base10
|
2022-05-11 08:51:59 +00:00
|
|
|
import pkg/stew/byteutils
|
2022-05-12 21:52:03 +00:00
|
|
|
import pkg/confutils
|
2022-01-10 15:32:56 +00:00
|
|
|
|
|
|
|
import pkg/libp2p/routing_record
|
2022-06-14 23:34:56 +00:00
|
|
|
import pkg/libp2pdht/discv5/spr as spr
|
2022-01-10 15:32:56 +00:00
|
|
|
|
|
|
|
import ../node
|
2022-04-06 00:34:29 +00:00
|
|
|
import ../blocktype
|
2022-05-12 21:52:03 +00:00
|
|
|
import ../conf
|
2022-04-21 08:12:16 +00:00
|
|
|
import ../contracts
|
2022-06-14 15:19:35 +00:00
|
|
|
import ../streams
|
2022-01-10 15:32:56 +00:00
|
|
|
|
2022-11-14 23:42:57 +00:00
|
|
|
import ./coders
|
2022-05-09 13:15:23 +00:00
|
|
|
import ./json
|
|
|
|
|
2022-11-14 23:42:57 +00:00
|
|
|
logScope:
|
|
|
|
topics = "codex restapi"
|
|
|
|
|
2022-01-10 15:32:56 +00:00
|
|
|
proc validate(
|
|
|
|
pattern: string,
|
|
|
|
value: string): int
|
|
|
|
{.gcsafe, raises: [Defect].} =
|
|
|
|
0
|
|
|
|
|
2022-05-19 19:56:03 +00:00
|
|
|
proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
|
2022-01-10 15:32:56 +00:00
|
|
|
var router = RestRouter.init(validate)
|
|
|
|
router.api(
|
|
|
|
MethodGet,
|
2022-05-19 19:56:03 +00:00
|
|
|
"/api/codex/v1/connect/{peerId}") do (
|
2023-03-10 07:02:54 +00:00
|
|
|
peerId: PeerId,
|
2022-01-10 15:32:56 +00:00
|
|
|
addrs: seq[MultiAddress]) -> RestApiResponse:
|
2022-01-10 20:35:52 +00:00
|
|
|
## 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
|
|
|
|
##
|
2023-03-15 13:10:53 +00:00
|
|
|
## `addrs` the listening addresses of the peers to dial, eg the one specified with `--listen-addrs`
|
|
|
|
##
|
2022-01-10 20:35:52 +00:00
|
|
|
|
2022-01-10 15:32:56 +00:00
|
|
|
if peerId.isErr:
|
|
|
|
return RestApiResponse.error(
|
|
|
|
Http400,
|
|
|
|
$peerId.error())
|
|
|
|
|
|
|
|
let addresses = if addrs.isOk and addrs.get().len > 0:
|
|
|
|
addrs.get()
|
|
|
|
else:
|
2022-04-13 16:32:35 +00:00
|
|
|
without peerRecord =? (await node.findPeer(peerId.get())):
|
2022-01-10 15:32:56 +00:00
|
|
|
return RestApiResponse.error(
|
|
|
|
Http400,
|
|
|
|
"Unable to find Peer!")
|
2022-04-13 16:32:35 +00:00
|
|
|
peerRecord.addresses.mapIt(it.address)
|
2022-01-20 01:07:30 +00:00
|
|
|
try:
|
|
|
|
await node.connect(peerId.get(), addresses)
|
|
|
|
return RestApiResponse.response("Successfully connected to peer")
|
2023-03-09 11:23:45 +00:00
|
|
|
except DialFailedError:
|
2022-01-20 01:07:30 +00:00
|
|
|
return RestApiResponse.error(Http400, "Unable to dial peer")
|
2023-03-09 11:23:45 +00:00
|
|
|
except CatchableError:
|
2022-01-20 01:07:30 +00:00
|
|
|
return RestApiResponse.error(Http400, "Unknown error dialling peer")
|
2022-01-10 15:32:56 +00:00
|
|
|
|
|
|
|
router.api(
|
|
|
|
MethodGet,
|
2022-05-19 19:56:03 +00:00
|
|
|
"/api/codex/v1/download/{id}") do (
|
2022-01-10 15:32:56 +00:00
|
|
|
id: Cid, resp: HttpResponseRef) -> RestApiResponse:
|
2022-01-10 20:35:52 +00:00
|
|
|
## Download a file from the node in a streaming
|
|
|
|
## manner
|
|
|
|
##
|
|
|
|
|
2022-01-10 15:32:56 +00:00
|
|
|
if id.isErr:
|
|
|
|
return RestApiResponse.error(
|
|
|
|
Http400,
|
|
|
|
$id.error())
|
|
|
|
|
2022-03-30 02:43:35 +00:00
|
|
|
var
|
|
|
|
stream: LPStream
|
2022-01-10 15:32:56 +00:00
|
|
|
|
|
|
|
var bytes = 0
|
|
|
|
try:
|
2022-04-06 00:34:29 +00:00
|
|
|
without stream =? (await node.retrieve(id.get())), error:
|
|
|
|
return RestApiResponse.error(Http404, error.msg)
|
2022-01-10 15:32:56 +00:00
|
|
|
|
2022-01-20 01:07:30 +00:00
|
|
|
resp.addHeader("Content-Type", "application/octet-stream")
|
2022-01-10 15:32:56 +00:00
|
|
|
await resp.prepareChunked()
|
2022-03-30 02:43:35 +00:00
|
|
|
|
2022-01-10 15:32:56 +00:00
|
|
|
while not stream.atEof:
|
|
|
|
var
|
2022-04-06 00:34:29 +00:00
|
|
|
buff = newSeqUninitialized[byte](BlockSize)
|
2022-01-10 15:32:56 +00:00
|
|
|
len = await stream.readOnce(addr buff[0], buff.len)
|
|
|
|
|
|
|
|
buff.setLen(len)
|
|
|
|
if buff.len <= 0:
|
|
|
|
break
|
|
|
|
|
|
|
|
bytes += buff.len
|
2022-02-03 14:02:18 +00:00
|
|
|
trace "Sending chunk", size = buff.len
|
2022-01-10 15:32:56 +00:00
|
|
|
await resp.sendChunk(addr buff[0], buff.len)
|
2022-02-03 14:02:18 +00:00
|
|
|
await resp.finish()
|
2022-01-10 15:32:56 +00:00
|
|
|
except CatchableError as exc:
|
|
|
|
trace "Excepting streaming blocks", exc = exc.msg
|
|
|
|
return RestApiResponse.error(Http500)
|
|
|
|
finally:
|
2022-11-14 23:42:57 +00:00
|
|
|
trace "Sent bytes", cid = id.get(), bytes
|
2022-03-30 02:43:35 +00:00
|
|
|
if not stream.isNil:
|
|
|
|
await stream.close()
|
2022-01-10 15:32:56 +00:00
|
|
|
|
2022-05-10 12:13:39 +00:00
|
|
|
router.rawApi(
|
2022-04-06 00:34:29 +00:00
|
|
|
MethodPost,
|
2022-05-19 19:56:03 +00:00
|
|
|
"/api/codex/v1/storage/request/{cid}") do (cid: Cid) -> RestApiResponse:
|
2022-04-06 00:34:29 +00:00
|
|
|
## Create a request for storage
|
|
|
|
##
|
2023-03-15 13:10:53 +00:00
|
|
|
## cid - the cid of a previously uploaded dataset
|
|
|
|
## duration - the duration of the request in seconds
|
|
|
|
## reward - the maximum amount of tokens paid per second per slot to hosts the client is willing to pay
|
|
|
|
## expiry - timestamp, in seconds, when the request expires if the Request does not find requested amount of nodes to host the data
|
|
|
|
## nodes - minimal number of nodes the content should be stored on
|
|
|
|
## tolerance - allowed number of nodes that can be lost before pronouncing the content lost
|
2022-04-06 00:34:29 +00:00
|
|
|
|
2022-05-10 12:13:39 +00:00
|
|
|
without cid =? cid.tryGet.catch, error:
|
|
|
|
return RestApiResponse.error(Http400, error.msg)
|
2022-04-06 00:34:29 +00:00
|
|
|
|
2022-05-10 12:13:39 +00:00
|
|
|
let body = await request.getBody()
|
2022-04-06 00:34:29 +00:00
|
|
|
|
2022-05-10 12:13:39 +00:00
|
|
|
without params =? StorageRequestParams.fromJson(body), error:
|
|
|
|
return RestApiResponse.error(Http400, error.msg)
|
|
|
|
|
2022-08-02 15:50:57 +00:00
|
|
|
let nodes = params.nodes |? 1
|
2023-03-13 12:23:50 +00:00
|
|
|
let tolerance = params.tolerance |? 0
|
2022-08-02 15:50:57 +00:00
|
|
|
|
2022-11-14 23:42:57 +00:00
|
|
|
without purchaseId =? await node.requestStorage(
|
|
|
|
cid,
|
|
|
|
params.duration,
|
|
|
|
nodes,
|
|
|
|
tolerance,
|
|
|
|
params.reward,
|
|
|
|
params.expiry), error:
|
|
|
|
|
2022-05-10 12:13:39 +00:00
|
|
|
return RestApiResponse.error(Http500, error.msg)
|
|
|
|
|
2022-05-11 07:01:31 +00:00
|
|
|
return RestApiResponse.response(purchaseId.toHex)
|
2022-04-06 00:34:29 +00:00
|
|
|
|
2022-01-10 15:32:56 +00:00
|
|
|
router.rawApi(
|
|
|
|
MethodPost,
|
2022-05-19 19:56:03 +00:00
|
|
|
"/api/codex/v1/upload") do (
|
2022-01-10 15:32:56 +00:00
|
|
|
) -> RestApiResponse:
|
2023-03-15 13:10:53 +00:00
|
|
|
## Upload a file in a streaming manner
|
2022-01-10 20:35:52 +00:00
|
|
|
##
|
|
|
|
|
2022-01-10 15:32:56 +00:00
|
|
|
trace "Handling file upload"
|
|
|
|
var bodyReader = request.getBodyReader()
|
|
|
|
if bodyReader.isErr():
|
|
|
|
return RestApiResponse.error(Http500)
|
|
|
|
|
|
|
|
# Attempt to handle `Expect` header
|
2022-01-10 20:35:52 +00:00
|
|
|
# some clients (curl), wait 1000ms
|
2022-01-10 15:32:56 +00:00
|
|
|
# before giving up
|
|
|
|
#
|
|
|
|
await request.handleExpect()
|
|
|
|
|
|
|
|
let
|
|
|
|
reader = bodyReader.get()
|
|
|
|
|
|
|
|
try:
|
2022-06-14 15:19:35 +00:00
|
|
|
without cid =? (
|
|
|
|
await node.store(AsyncStreamWrapper.new(reader = AsyncStreamReader(reader)))), error:
|
|
|
|
trace "Error uploading file", exc = error.msg
|
2022-04-06 00:34:29 +00:00
|
|
|
return RestApiResponse.error(Http500, error.msg)
|
2022-01-10 15:32:56 +00:00
|
|
|
|
2022-10-27 13:41:34 +00:00
|
|
|
trace "Uploaded file", cid
|
2022-01-10 15:32:56 +00:00
|
|
|
return RestApiResponse.response($cid)
|
2023-03-09 11:23:45 +00:00
|
|
|
except CancelledError:
|
2022-01-10 15:32:56 +00:00
|
|
|
return RestApiResponse.error(Http500)
|
|
|
|
except AsyncStreamError:
|
|
|
|
return RestApiResponse.error(Http500)
|
|
|
|
finally:
|
|
|
|
await reader.closeWait()
|
|
|
|
|
|
|
|
# if we got here something went wrong?
|
|
|
|
return RestApiResponse.error(Http500)
|
|
|
|
|
2022-11-14 23:42:57 +00:00
|
|
|
router.api(
|
|
|
|
MethodPost,
|
|
|
|
"/api/codex/v1/debug/chronicles/loglevel") do (
|
|
|
|
level: Option[string]) -> RestApiResponse:
|
|
|
|
## Set log level at run time
|
|
|
|
##
|
|
|
|
## e.g. `chronicles/loglevel?level=DEBUG`
|
|
|
|
##
|
|
|
|
## `level` - chronicles log level
|
|
|
|
##
|
|
|
|
|
|
|
|
without res =? level and level =? res:
|
|
|
|
return RestApiResponse.error(Http400, "Missing log level")
|
|
|
|
|
|
|
|
try:
|
|
|
|
{.gcsafe.}:
|
|
|
|
updateLogLevel(level)
|
|
|
|
except CatchableError as exc:
|
|
|
|
return RestApiResponse.error(Http500, exc.msg)
|
|
|
|
|
|
|
|
return RestApiResponse.response("")
|
|
|
|
|
2022-01-10 15:32:56 +00:00
|
|
|
router.api(
|
|
|
|
MethodGet,
|
2022-11-14 23:42:57 +00:00
|
|
|
"/api/codex/v1/debug/info") do () -> RestApiResponse:
|
2022-01-10 20:35:52 +00:00
|
|
|
## Print rudimentary node information
|
|
|
|
##
|
|
|
|
|
2022-11-02 00:58:41 +00:00
|
|
|
let
|
|
|
|
json = %*{
|
|
|
|
"id": $node.switch.peerInfo.peerId,
|
|
|
|
"addrs": node.switch.peerInfo.addrs.mapIt( $it ),
|
|
|
|
"repo": $conf.dataDir,
|
|
|
|
"spr":
|
|
|
|
if node.discovery.dhtRecord.isSome:
|
|
|
|
node.discovery.dhtRecord.get.toURI
|
|
|
|
else:
|
2023-03-08 11:45:55 +00:00
|
|
|
"",
|
|
|
|
"codex": {
|
|
|
|
"version": $codexVersion,
|
|
|
|
"revision": $codexRevision
|
|
|
|
}
|
2022-11-02 00:58:41 +00:00
|
|
|
}
|
2022-01-10 15:32:56 +00:00
|
|
|
|
2022-06-14 23:34:56 +00:00
|
|
|
return RestApiResponse.response($json)
|
2022-01-10 15:32:56 +00:00
|
|
|
|
2022-05-09 14:51:08 +00:00
|
|
|
router.api(
|
|
|
|
MethodGet,
|
2022-05-19 19:56:03 +00:00
|
|
|
"/api/codex/v1/sales/availability") do () -> RestApiResponse:
|
2022-05-09 14:51:08 +00:00
|
|
|
## Returns storage that is for sale
|
|
|
|
|
|
|
|
without contracts =? node.contracts:
|
|
|
|
return RestApiResponse.error(Http503, "Sales unavailable")
|
|
|
|
|
|
|
|
let json = %contracts.sales.available
|
|
|
|
return RestApiResponse.response($json)
|
|
|
|
|
2022-05-09 13:15:23 +00:00
|
|
|
router.rawApi(
|
|
|
|
MethodPost,
|
2022-05-19 19:56:03 +00:00
|
|
|
"/api/codex/v1/sales/availability") do () -> RestApiResponse:
|
2022-04-21 08:12:16 +00:00
|
|
|
## 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)
|
|
|
|
|
2022-05-09 14:51:08 +00:00
|
|
|
without contracts =? node.contracts:
|
|
|
|
return RestApiResponse.error(Http503, "Sales unavailable")
|
|
|
|
|
2022-05-09 13:15:23 +00:00
|
|
|
let body = await request.getBody()
|
2022-04-21 08:12:16 +00:00
|
|
|
|
2022-05-09 13:15:23 +00:00
|
|
|
without availability =? Availability.fromJson(body), error:
|
|
|
|
return RestApiResponse.error(Http400, error.msg)
|
2022-04-21 08:12:16 +00:00
|
|
|
|
|
|
|
contracts.sales.add(availability)
|
2022-05-09 14:51:08 +00:00
|
|
|
|
|
|
|
let json = %availability
|
|
|
|
return RestApiResponse.response($json)
|
2022-04-21 08:12:16 +00:00
|
|
|
|
2022-05-11 08:51:59 +00:00
|
|
|
router.api(
|
|
|
|
MethodGet,
|
2022-05-19 19:56:03 +00:00
|
|
|
"/api/codex/v1/storage/purchases/{id}") do (
|
2022-08-17 02:29:44 +00:00
|
|
|
id: PurchaseId) -> RestApiResponse:
|
2022-05-11 08:51:59 +00:00
|
|
|
|
|
|
|
without contracts =? node.contracts:
|
|
|
|
return RestApiResponse.error(Http503, "Purchasing unavailable")
|
|
|
|
|
|
|
|
without id =? id.tryGet.catch, error:
|
|
|
|
return RestApiResponse.error(Http400, error.msg)
|
|
|
|
|
|
|
|
without purchase =? contracts.purchasing.getPurchase(id):
|
|
|
|
return RestApiResponse.error(Http404)
|
|
|
|
|
|
|
|
let json = %purchase
|
|
|
|
|
|
|
|
return RestApiResponse.response($json)
|
|
|
|
|
|
|
|
|
2022-01-10 15:32:56 +00:00
|
|
|
return router
|