Adds endpoint for listing files (manifests) in node. Useful for demo UI. (#599)

* Adds endpoint for listing files (manifests) in node. Useful for demo UI.

* Moves upload/download/files into content API calls.

* Cleans up json serialization for manifest

* Cleans up some more json serialization

* Moves block iteration and decoding to node.nim

* Moves api methods into their own init procs.

* Applies RestContent api object.

* Replaces format methods with Rest objects in json.nim

* Unused import

* Review comments by Adam

* Fixes issue where content/local endpoint clashes with content/cid.

* faulty merge resolution

* Renames content API to data.

* Fixes faulty rebase

* Adds test for data/local API

* Renames local and download api.
This commit is contained in:
Ben Bierens 2023-11-09 09:47:09 +01:00 committed by GitHub
parent 7d4ea878d2
commit cb02962231
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 400 additions and 252 deletions

View File

@ -21,6 +21,7 @@ import pkg/chronicles
import ../errors import ../errors
import ../utils import ../utils
import ../utils/json
import ../units import ../units
import ../blocktype import ../blocktype
import ./types import ./types
@ -29,14 +30,14 @@ export types
type type
Manifest* = ref object of RootObj Manifest* = ref object of RootObj
rootHash: ?Cid # Root (tree) hash of the contained data set rootHash {.serialize.}: ?Cid # Root (tree) hash of the contained data set
originalBytes*: NBytes # Exact size of the original (uploaded) file originalBytes* {.serialize.}: NBytes # Exact size of the original (uploaded) file
blockSize: NBytes # Size of each contained block (might not be needed if blocks are len-prefixed) blockSize {.serialize.}: NBytes # Size of each contained block (might not be needed if blocks are len-prefixed)
blocks: seq[Cid] # Block Cid blocks: seq[Cid] # Block Cid
version: CidVersion # Cid version version: CidVersion # Cid version
hcodec: MultiCodec # Multihash codec hcodec: MultiCodec # Multihash codec
codec: MultiCodec # Data set codec codec: MultiCodec # Data set codec
case protected: bool # Protected datasets have erasure coded info case protected {.serialize.}: bool # Protected datasets have erasure coded info
of true: of true:
ecK: int # Number of blocks to encode ecK: int # Number of blocks to encode
ecM: int # Number of resulting parity blocks ecM: int # Number of resulting parity blocks

View File

@ -60,6 +60,8 @@ type
discovery*: Discovery discovery*: Discovery
contracts*: Contracts contracts*: Contracts
OnManifest* = proc(cid: Cid, manifest: Manifest): void {.gcsafe, closure.}
proc findPeer*( proc findPeer*(
node: CodexNodeRef, node: CodexNodeRef,
peerId: PeerId): Future[?PeerRecord] {.async.} = peerId: PeerId): Future[?PeerRecord] {.async.} =
@ -235,6 +237,23 @@ proc store*(
return manifest.cid.success return manifest.cid.success
proc iterateManifests*(node: CodexNodeRef, onManifest: OnManifest) {.async.} =
without cids =? await node.blockStore.listBlocks(BlockType.Manifest):
warn "Failed to listBlocks"
return
for c in cids:
if cid =? await c:
without blk =? await node.blockStore.getBlock(cid):
warn "Failed to get manifest block by cid", cid
return
without manifest =? Manifest.decode(blk):
warn "Failed to decode manifest", cid
return
onManifest(cid, manifest)
proc requestStorage*( proc requestStorage*(
self: CodexNodeRef, self: CodexNodeRef,
cid: Cid, cid: Cid,

View File

@ -28,8 +28,6 @@ import pkg/confutils
import pkg/libp2p import pkg/libp2p
import pkg/libp2p/routing_record import pkg/libp2p/routing_record
import pkg/codexdht/discv5/spr as spr import pkg/codexdht/discv5/spr as spr
import pkg/codexdht/discv5/routing_table as rt
import pkg/codexdht/discv5/node as dn
import ../node import ../node
import ../blocktype import ../blocktype
@ -52,173 +50,20 @@ proc validate(
{.gcsafe, raises: [Defect].} = {.gcsafe, raises: [Defect].} =
0 0
proc formatAddress(address: Option[dn.Address]): string = proc formatManifestBlocks(node: CodexNodeRef): Future[JsonNode] {.async.} =
if address.isSome(): var content: seq[RestContent] = @[]
return $address.get()
return "<none>"
proc formatNode(node: dn.Node): JsonNode = proc formatManifest(cid: Cid, manifest: Manifest) =
let jobj = %*{ let restContent = RestContent.init(cid, manifest)
"nodeId": $node.id, content.add(restContent)
"peerId": $node.record.data.peerId,
"record": $node.record,
"address": formatAddress(node.address),
"seen": $node.seen
}
return jobj
proc formatTable(routingTable: rt.RoutingTable): JsonNode = await node.iterateManifests(formatManifest)
let jarray = newJArray() return %content
for bucket in routingTable.buckets:
for node in bucket.nodes:
jarray.add(formatNode(node))
let jobj = %*{
"localNode": formatNode(routingTable.localNode),
"nodes": jarray
}
return jobj
proc formatPeerRecord(peerRecord: PeerRecord): JsonNode =
let jarray = newJArray()
for maddr in peerRecord.addresses:
jarray.add(%*{
"address": $maddr.address
})
let jobj = %*{
"peerId": $peerRecord.peerId,
"seqNo": $peerRecord.seqNo,
"addresses": jarray
}
return jobj
proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
var router = RestRouter.init(validate)
router.api(
MethodGet,
"/api/codex/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
##
## `addrs` the listening addresses of the peers to dial, eg the one specified with `--listen-addrs`
##
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:
return RestApiResponse.error(Http400, "Unable to dial peer")
except CatchableError:
return RestApiResponse.error(Http400, "Unknown error dialling peer")
router.api(
MethodGet,
"/api/codex/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](DefaultBlockSize.int)
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()
codex_api_downloads.inc()
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()
proc initDataApi(node: CodexNodeRef, router: var RestRouter) =
router.rawApi( router.rawApi(
MethodPost, MethodPost,
"/api/codex/v1/storage/request/{cid}") do (cid: Cid) -> RestApiResponse: "/api/codex/v1/data") do (
## Create a request for storage
##
## cid - the cid of a previously uploaded dataset
## duration - the duration of the request in seconds
## proofProbability - how often storage proofs are required
## 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
## colateral - requested collateral from hosts when they fill slot
without cid =? cid.tryGet.catch, error:
return RestApiResponse.error(Http400, error.msg)
let body = await request.getBody()
without params =? StorageRequestParams.fromJson(body), error:
return RestApiResponse.error(Http400, error.msg)
let nodes = params.nodes |? 1
let tolerance = params.tolerance |? 0
without purchaseId =? await node.requestStorage(
cid,
params.duration,
params.proofProbability,
nodes,
tolerance,
params.reward,
params.collateral,
params.expiry), error:
return RestApiResponse.error(Http500, error.msg)
return RestApiResponse.response(purchaseId.toHex)
router.rawApi(
MethodPost,
"/api/codex/v1/upload") do (
) -> RestApiResponse: ) -> RestApiResponse:
## Upload a file in a streaming manner ## Upload a file in a streaming manner
## ##
@ -259,68 +104,58 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
return RestApiResponse.error(Http500) return RestApiResponse.error(Http500)
router.api( router.api(
MethodPost, MethodGet,
"/api/codex/v1/debug/chronicles/loglevel") do ( "/api/codex/v1/local") do () -> RestApiResponse:
level: Option[string]) -> RestApiResponse: let json = await formatManifestBlocks(node)
## Set log level at run time return RestApiResponse.response($json, contentType="application/json")
##
## 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("")
router.api( router.api(
MethodGet, MethodGet,
"/api/codex/v1/debug/info") do () -> RestApiResponse: "/api/codex/v1/data/{cid}") do (
## Print rudimentary node information cid: Cid, resp: HttpResponseRef) -> RestApiResponse:
## Download a file from the node in a streaming
## manner
## ##
let if cid.isErr:
json = %*{ return RestApiResponse.error(
"id": $node.switch.peerInfo.peerId, Http400,
"addrs": node.switch.peerInfo.addrs.mapIt( $it ), $cid.error())
"repo": $conf.dataDir,
"spr":
if node.discovery.dhtRecord.isSome:
node.discovery.dhtRecord.get.toURI
else:
"",
"table": formatTable(node.discovery.protocol.routingTable),
"codex": {
"version": $codexVersion,
"revision": $codexRevision
}
}
return RestApiResponse.response($json, contentType="application/json") var
stream: LPStream
when codex_enable_api_debug_peers: var bytes = 0
router.api( try:
MethodGet, without stream =? (await node.retrieve(cid.get())), error:
"/api/codex/v1/debug/peer/{peerId}") do (peerId: PeerId) -> RestApiResponse: return RestApiResponse.error(Http404, error.msg)
trace "debug/peer start" resp.addHeader("Content-Type", "application/octet-stream")
without peerRecord =? (await node.findPeer(peerId.get())): await resp.prepareChunked()
trace "debug/peer peer not found!"
return RestApiResponse.error(
Http400,
"Unable to find Peer!")
let json = formatPeerRecord(peerRecord) while not stream.atEof:
trace "debug/peer returning peer record" var
return RestApiResponse.response($json) buff = newSeqUninitialized[byte](DefaultBlockSize.int)
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()
codex_api_downloads.inc()
except CatchableError as exc:
trace "Excepting streaming blocks", exc = exc.msg
return RestApiResponse.error(Http500)
finally:
trace "Sent bytes", cid = cid.get(), bytes
if not stream.isNil:
await stream.close()
proc initSalesApi(node: CodexNodeRef, router: var RestRouter) =
router.api( router.api(
MethodGet, MethodGet,
"/api/codex/v1/sales/slots") do () -> RestApiResponse: "/api/codex/v1/sales/slots") do () -> RestApiResponse:
@ -381,6 +216,46 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
return RestApiResponse.response(availability.toJson, return RestApiResponse.response(availability.toJson,
contentType="application/json") contentType="application/json")
proc initPurchasingApi(node: CodexNodeRef, router: var RestRouter) =
router.rawApi(
MethodPost,
"/api/codex/v1/storage/request/{cid}") do (cid: Cid) -> RestApiResponse:
## Create a request for storage
##
## cid - the cid of a previously uploaded dataset
## duration - the duration of the request in seconds
## proofProbability - how often storage proofs are required
## 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
## colateral - requested collateral from hosts when they fill slot
without cid =? cid.tryGet.catch, error:
return RestApiResponse.error(Http400, error.msg)
let body = await request.getBody()
without params =? StorageRequestParams.fromJson(body), error:
return RestApiResponse.error(Http400, error.msg)
let nodes = params.nodes |? 1
let tolerance = params.tolerance |? 0
without purchaseId =? await node.requestStorage(
cid,
params.duration,
params.proofProbability,
nodes,
tolerance,
params.reward,
params.collateral,
params.expiry), error:
return RestApiResponse.error(Http500, error.msg)
return RestApiResponse.response(purchaseId.toHex)
router.api( router.api(
MethodGet, MethodGet,
"/api/codex/v1/storage/purchases/{id}") do ( "/api/codex/v1/storage/purchases/{id}") do (
@ -404,4 +279,114 @@ proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
return RestApiResponse.response($json, contentType="application/json") return RestApiResponse.response($json, contentType="application/json")
proc initDebugApi(node: CodexNodeRef, conf: CodexConf, router: var RestRouter) =
router.api(
MethodGet,
"/api/codex/v1/debug/info") do () -> RestApiResponse:
## Print rudimentary node information
##
let table = RestRoutingTable.init(node.discovery.protocol.routingTable)
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:
"",
"table": table,
"codex": {
"version": $codexVersion,
"revision": $codexRevision
}
}
return RestApiResponse.response($json, contentType="application/json")
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("")
when codex_enable_api_debug_peers:
router.api(
MethodGet,
"/api/codex/v1/debug/peer/{peerId}") do (peerId: PeerId) -> RestApiResponse:
trace "debug/peer start"
without peerRecord =? (await node.findPeer(peerId.get())):
trace "debug/peer peer not found!"
return RestApiResponse.error(
Http400,
"Unable to find Peer!")
let json = %RestPeerRecord.init(peerRecord)
trace "debug/peer returning peer record"
return RestApiResponse.response($json)
proc initRestApi*(node: CodexNodeRef, conf: CodexConf): RestRouter =
var router = RestRouter.init(validate)
initDataApi(node, router)
initSalesApi(node, router)
initPurchasingApi(node, router)
initDebugApi(node, conf, router)
router.api(
MethodGet,
"/api/codex/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
##
## `addrs` the listening addresses of the peers to dial, eg the one specified with `--listen-addrs`
##
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:
return RestApiResponse.error(Http400, "Unable to dial peer")
except CatchableError:
return RestApiResponse.error(Http400, "Unknown error dialling peer")
return router return router

View File

@ -1,9 +1,14 @@
import pkg/questionable import pkg/questionable
import pkg/questionable/results import pkg/questionable/results
import pkg/stew/byteutils import pkg/stew/byteutils
import pkg/libp2p
import pkg/codexdht/discv5/node as dn
import pkg/codexdht/discv5/routing_table as rt
import ../sales import ../sales
import ../purchasing import ../purchasing
import ../utils/json import ../utils/json
import ../units
import ../manifest
export json export json
@ -29,9 +34,77 @@ type
minPrice* {.serialize.}: UInt256 minPrice* {.serialize.}: UInt256
maxCollateral* {.serialize.}: UInt256 maxCollateral* {.serialize.}: UInt256
RestContent* = object
cid* {.serialize.}: Cid
manifest* {.serialize.}: Manifest
RestNode* = object
nodeId* {.serialize.}: NodeId
peerId* {.serialize.}: PeerId
record* {.serialize.}: SignedPeerRecord
address* {.serialize.}: Option[dn.Address]
seen* {.serialize.}: bool
RestRoutingTable* = object
localNode* {.serialize.}: RestNode
nodes* {.serialize.}: seq[RestNode]
RestPeerRecord* = object
peerId* {.serialize.}: PeerId
seqNo* {.serialize.}: uint64
addresses* {.serialize.}: seq[AddressInfo]
proc init*(_: type RestContent, cid: Cid, manifest: Manifest): RestContent =
RestContent(
cid: cid,
manifest: manifest
)
proc init*(_: type RestNode, node: dn.Node): RestNode =
RestNode(
nodeId: node.id,
peerId: node.record.data.peerId,
record: node.record,
address: node.address,
seen: node.seen
)
proc init*(_: type RestRoutingTable, routingTable: rt.RoutingTable): RestRoutingTable =
var nodes: seq[RestNode] = @[]
for bucket in routingTable.buckets:
for node in bucket.nodes:
nodes.add(RestNode.init(node))
RestRoutingTable(
localNode: RestNode.init(routingTable.localNode),
nodes: nodes
)
proc init*(_: type RestPeerRecord, peerRecord: PeerRecord): RestPeerRecord =
RestPeerRecord(
peerId: peerRecord.peerId,
seqNo: peerRecord.seqNo,
addresses: peerRecord.addresses
)
func `%`*(obj: StorageRequest | Slot): JsonNode = func `%`*(obj: StorageRequest | Slot): JsonNode =
let jsonObj = newJObject() let jsonObj = newJObject()
for k, v in obj.fieldPairs: jsonObj[k] = %v for k, v in obj.fieldPairs: jsonObj[k] = %v
jsonObj["id"] = %(obj.id) jsonObj["id"] = %(obj.id)
return jsonObj return jsonObj
func `%`*(obj: Cid): JsonNode =
% $obj
func `%`*(obj: PeerId): JsonNode =
% $obj
func `%`*(obj: SignedPeerRecord): JsonNode =
% $obj
func `%`*(obj: dn.Address): JsonNode =
% $obj
func `%`*(obj: AddressInfo): JsonNode =
% $obj.address

View File

@ -80,6 +80,11 @@ method ensureExpiry*(
return success() return success()
method listBlocks*(
self: NetworkStore,
blockType = BlockType.Manifest): Future[?!BlocksIter] =
self.localStore.listBlocks(blockType)
method delBlock*(self: NetworkStore, cid: Cid): Future[?!void] = method delBlock*(self: NetworkStore, cid: Cid): Future[?!void] =
## Delete a block from the blockstore ## Delete a block from the blockstore
## ##

View File

@ -110,7 +110,7 @@ We're now ready to upload a file to the network. In this example we'll use node
Replace `<FILE PATH>` with the path to the file you want to upload in the following command: Replace `<FILE PATH>` with the path to the file you want to upload in the following command:
```bash ```bash
curl -H "content-type: application/octet-stream" -H "Expect: 100-continue" -T "<FILE PATH>" 127.0.0.1:8080/api/codex/v1/upload -X POST curl -H "content-type: application/octet-stream" -H "Expect: 100-continue" -T "<FILE PATH>" 127.0.0.1:8080/api/codex/v1/content -X POST
``` ```
(Hint: if curl is reluctant to show you the response, add `-o <FILENAME>` to write the result to a file.) (Hint: if curl is reluctant to show you the response, add `-o <FILENAME>` to write the result to a file.)
@ -122,7 +122,7 @@ Depending on the file size this may take a moment. Codex is processing the file
Replace `<CID>` with the identifier returned in the previous step. Replace `<OUTPUT FILE>` with the filename where you want to store the downloaded file. Replace `<CID>` with the identifier returned in the previous step. Replace `<OUTPUT FILE>` with the filename where you want to store the downloaded file.
```bash ```bash
curl 127.0.0.1:8081/api/codex/v1/download/zdj7Wfm18wewSWL9SPqddhJuu5ii1TJD39rtt3JbVYdKcqM1K --output <OUTPUT FILE> curl 127.0.0.1:8081/api/codex/v1/content/zdj7Wfm18wewSWL9SPqddhJuu5ii1TJD39rtt3JbVYdKcqM1K --output <OUTPUT FILE>
``` ```
Notice we are connecting to the second node in order to download the file. The CID we provide contains the information needed to locate the file within the network. Notice we are connecting to the second node in order to download the file. The CID we provide contains the information needed to locate the file within the network.

View File

@ -205,6 +205,38 @@ components:
request: request:
$ref: "#/components/schemas/StorageRequest" $ref: "#/components/schemas/StorageRequest"
DataList:
type: object
properties:
content:
type: array
items:
$ref: "#/components/schemas/DataItem"
DataItem:
type: object
properties:
cid:
$ref: "#/components/schemas/Cid"
manifest:
$ref: "#/components/schemas/ManifestItem"
ManifestItem:
type: object
properties:
rootHash:
$ref: "#/components/schemas/Cid"
description: "Root hash of the content"
originalBytes:
type: number
description: "Length of original content in bytes"
blockSize:
type: number
description: "Size of blocks"
protected:
type: boolean
description: "Indicates if content is protected by erasure-coding"
servers: servers:
- url: "http://localhost:8080/api/codex/v1" - url: "http://localhost:8080/api/codex/v1"
@ -252,9 +284,51 @@ paths:
"400": "400":
description: Peer either not found or was not possible to dial description: Peer either not found or was not possible to dial
"/download/{cid}": "/local":
get: get:
summary: "Download a file from the node in a streaming manner" summary: "Lists manifest CIDs stored locally in node."
tags: [ Data ]
operationId: listData
responses:
"200":
description: Retrieved list of content CIDs
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/DataList"
"400":
description: Invalid CID is specified
"404":
description: Content specified by the CID is not found
"500":
description: Well it was bad-bad
"/data":
post:
summary: "Upload a file in a streaming manner. Once finished, the file is stored in the node and can be retrieved by any node in the network using the returned CID."
tags: [ Data ]
operationId: upload
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
"200":
description: CID of uploaded file
content:
text/plain:
schema:
type: string
"500":
description: Well it was bad-bad and the upload did not work out
"/data/{cid}":
get:
summary: "Download a file from the node in a streaming manner. If the file is not available locally, it will be retrieved from other nodes in the network if able."
tags: [ Data ] tags: [ Data ]
operationId: download operationId: download
parameters: parameters:
@ -280,27 +354,6 @@ paths:
"500": "500":
description: Well it was bad-bad description: Well it was bad-bad
"/upload":
post:
summary: "Upload a file in a streaming manner"
tags: [ Data ]
operationId: upload
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
"200":
description: CID of uploaded file
content:
text/plain:
schema:
type: string
"500":
description: Well it was bad-bad and the upload did not work out
"/sales/slots": "/sales/slots":
get: get:
summary: "Returns active slots" summary: "Returns active slots"

View File

@ -27,7 +27,7 @@ proc setLogLevel*(client: CodexClient, level: string) =
assert response.status == "200 OK" assert response.status == "200 OK"
proc upload*(client: CodexClient, contents: string): ?!Cid = proc upload*(client: CodexClient, contents: string): ?!Cid =
let response = client.http.post(client.baseurl & "/upload", contents) let response = client.http.post(client.baseurl & "/data", contents)
assert response.status == "200 OK" assert response.status == "200 OK"
Cid.init(response.body).mapFailure Cid.init(response.body).mapFailure

View File

@ -1,5 +1,6 @@
import std/os import std/os
import std/httpclient import std/httpclient
import std/strutils
from std/net import TimeoutError from std/net import TimeoutError
import pkg/chronos import pkg/chronos
@ -37,7 +38,7 @@ ethersuite "Node block expiration tests":
proc uploadTestFile(): string = proc uploadTestFile(): string =
let client = newHttpClient() let client = newHttpClient()
let uploadUrl = baseurl & "/upload" let uploadUrl = baseurl & "/data"
let uploadResponse = client.post(uploadUrl, content) let uploadResponse = client.post(uploadUrl, content)
check uploadResponse.status == "200 OK" check uploadResponse.status == "200 OK"
client.close() client.close()
@ -45,11 +46,18 @@ ethersuite "Node block expiration tests":
proc downloadTestFile(contentId: string): Response = proc downloadTestFile(contentId: string): Response =
let client = newHttpClient(timeout=3000) let client = newHttpClient(timeout=3000)
let downloadUrl = baseurl & "/download/" & contentId let downloadUrl = baseurl & "/data/" & contentId
let content = client.get(downloadUrl) let content = client.get(downloadUrl)
client.close() client.close()
content content
proc hasFile(contentId: string): bool =
let client = newHttpClient(timeout=3000)
let dataLocalUrl = baseurl & "/local"
let content = client.get(dataLocalUrl)
client.close()
return content.body.contains(contentId)
test "node retains not-expired file": test "node retains not-expired file":
startTestNode(blockTtlSeconds = 10) startTestNode(blockTtlSeconds = 10)
@ -59,6 +67,7 @@ ethersuite "Node block expiration tests":
let response = downloadTestFile(contentId) let response = downloadTestFile(contentId)
check: check:
hasFile(contentId)
response.status == "200 OK" response.status == "200 OK"
response.body == content response.body == content
@ -69,5 +78,8 @@ ethersuite "Node block expiration tests":
await sleepAsync(3.seconds) await sleepAsync(3.seconds)
check:
not hasFile(contentId)
expect TimeoutError: expect TimeoutError:
discard downloadTestFile(contentId) discard downloadTestFile(contentId)