Merge branch 'master' into fix/rest/remove-duplicated-header-for-download

This commit is contained in:
Giuliano Mega 2024-10-30 14:01:47 -03:00 committed by GitHub
commit c349bfe5c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 415 additions and 113 deletions

View File

@ -28,14 +28,13 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: '0' fetch-depth: 0
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 18
- name: Lint OpenAPI - name: Lint OpenAPI
shell: bash
run: npx @redocly/cli lint openapi.yaml run: npx @redocly/cli lint openapi.yaml
deploy: deploy:
@ -46,20 +45,22 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: '0' fetch-depth: 0
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 18
- name: Build OpenAPI - name: Build OpenAPI
shell: bash run: npx @redocly/cli build-docs openapi.yaml --output openapi/index.html --title "Codex API"
run: npx @redocly/cli build-docs openapi.yaml --output "openapi/index.html" --title "Codex API"
- name: Build Postman Collection
run: npx -y openapi-to-postmanv2 -s openapi.yaml -o openapi/postman.json -p -O folderStrategy=Tags,includeAuthInfoInExample=false
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v3
with: with:
path: './openapi' path: openapi
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4 uses: actions/deploy-pages@v4

View File

@ -56,7 +56,7 @@ proc approveFunds(market: OnChainMarket, amount: UInt256) {.async.} =
discard await token.increaseAllowance(market.contract.address(), amount).confirm(0) discard await token.increaseAllowance(market.contract.address(), amount).confirm(0)
method getZkeyHash*(market: OnChainMarket): Future[?string] {.async.} = method getZkeyHash*(market: OnChainMarket): Future[?string] {.async.} =
let config = await market.contract.config() let config = await market.contract.configuration()
return some config.proofs.zkeyHash return some config.proofs.zkeyHash
method getSigner*(market: OnChainMarket): Future[Address] {.async.} = method getSigner*(market: OnChainMarket): Future[Address] {.async.} =
@ -65,18 +65,18 @@ method getSigner*(market: OnChainMarket): Future[Address] {.async.} =
method periodicity*(market: OnChainMarket): Future[Periodicity] {.async.} = method periodicity*(market: OnChainMarket): Future[Periodicity] {.async.} =
convertEthersError: convertEthersError:
let config = await market.contract.config() let config = await market.contract.configuration()
let period = config.proofs.period let period = config.proofs.period
return Periodicity(seconds: period) return Periodicity(seconds: period)
method proofTimeout*(market: OnChainMarket): Future[UInt256] {.async.} = method proofTimeout*(market: OnChainMarket): Future[UInt256] {.async.} =
convertEthersError: convertEthersError:
let config = await market.contract.config() let config = await market.contract.configuration()
return config.proofs.timeout return config.proofs.timeout
method proofDowntime*(market: OnChainMarket): Future[uint8] {.async.} = method proofDowntime*(market: OnChainMarket): Future[uint8] {.async.} =
convertEthersError: convertEthersError:
let config = await market.contract.config() let config = await market.contract.configuration()
return config.proofs.downtime return config.proofs.downtime
method getPointer*(market: OnChainMarket, slotId: SlotId): Future[uint8] {.async.} = method getPointer*(market: OnChainMarket, slotId: SlotId): Future[uint8] {.async.} =
@ -176,7 +176,7 @@ method fillSlot(market: OnChainMarket,
method freeSlot*(market: OnChainMarket, slotId: SlotId) {.async.} = method freeSlot*(market: OnChainMarket, slotId: SlotId) {.async.} =
convertEthersError: convertEthersError:
var freeSlot: Future[?TransactionResponse] var freeSlot: Future[Confirmable]
if rewardRecipient =? market.rewardRecipient: if rewardRecipient =? market.rewardRecipient:
# If --reward-recipient specified, use it as the reward recipient, and use # If --reward-recipient specified, use it as the reward recipient, and use
# the SP's address as the collateral recipient # the SP's address as the collateral recipient

View File

@ -17,18 +17,18 @@ export requests
type type
Marketplace* = ref object of Contract Marketplace* = ref object of Contract
proc config*(marketplace: Marketplace): MarketplaceConfig {.contract, view.} proc configuration*(marketplace: Marketplace): MarketplaceConfig {.contract, view.}
proc token*(marketplace: Marketplace): Address {.contract, view.} proc token*(marketplace: Marketplace): Address {.contract, view.}
proc slashMisses*(marketplace: Marketplace): UInt256 {.contract, view.} proc slashMisses*(marketplace: Marketplace): UInt256 {.contract, view.}
proc slashPercentage*(marketplace: Marketplace): UInt256 {.contract, view.} proc slashPercentage*(marketplace: Marketplace): UInt256 {.contract, view.}
proc minCollateralThreshold*(marketplace: Marketplace): UInt256 {.contract, view.} proc minCollateralThreshold*(marketplace: Marketplace): UInt256 {.contract, view.}
proc requestStorage*(marketplace: Marketplace, request: StorageRequest): ?TransactionResponse {.contract.} proc requestStorage*(marketplace: Marketplace, request: StorageRequest): Confirmable {.contract.}
proc fillSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256, proof: Groth16Proof): ?TransactionResponse {.contract.} proc fillSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256, proof: Groth16Proof): Confirmable {.contract.}
proc withdrawFunds*(marketplace: Marketplace, requestId: RequestId): ?TransactionResponse {.contract.} proc withdrawFunds*(marketplace: Marketplace, requestId: RequestId): Confirmable {.contract.}
proc withdrawFunds*(marketplace: Marketplace, requestId: RequestId, withdrawAddress: Address): ?TransactionResponse {.contract.} proc withdrawFunds*(marketplace: Marketplace, requestId: RequestId, withdrawAddress: Address): Confirmable {.contract.}
proc freeSlot*(marketplace: Marketplace, id: SlotId): ?TransactionResponse {.contract.} proc freeSlot*(marketplace: Marketplace, id: SlotId): Confirmable {.contract.}
proc freeSlot*(marketplace: Marketplace, id: SlotId, rewardRecipient: Address, collateralRecipient: Address): ?TransactionResponse {.contract.} proc freeSlot*(marketplace: Marketplace, id: SlotId, rewardRecipient: Address, collateralRecipient: Address): Confirmable {.contract.}
proc getRequest*(marketplace: Marketplace, id: RequestId): StorageRequest {.contract, view.} proc getRequest*(marketplace: Marketplace, id: RequestId): StorageRequest {.contract, view.}
proc getHost*(marketplace: Marketplace, id: SlotId): Address {.contract, view.} proc getHost*(marketplace: Marketplace, id: SlotId): Address {.contract, view.}
proc getActiveSlot*(marketplace: Marketplace, id: SlotId): Slot {.contract, view.} proc getActiveSlot*(marketplace: Marketplace, id: SlotId): Slot {.contract, view.}
@ -49,8 +49,8 @@ proc willProofBeRequired*(marketplace: Marketplace, id: SlotId): bool {.contract
proc getChallenge*(marketplace: Marketplace, id: SlotId): array[32, byte] {.contract, view.} proc getChallenge*(marketplace: Marketplace, id: SlotId): array[32, byte] {.contract, view.}
proc getPointer*(marketplace: Marketplace, id: SlotId): uint8 {.contract, view.} proc getPointer*(marketplace: Marketplace, id: SlotId): uint8 {.contract, view.}
proc submitProof*(marketplace: Marketplace, id: SlotId, proof: Groth16Proof): ?TransactionResponse {.contract.} proc submitProof*(marketplace: Marketplace, id: SlotId, proof: Groth16Proof): Confirmable {.contract.}
proc markProofAsMissing*(marketplace: Marketplace, id: SlotId, period: UInt256): ?TransactionResponse {.contract.} proc markProofAsMissing*(marketplace: Marketplace, id: SlotId, period: UInt256): Confirmable {.contract.}
proc reserveSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256): ?TransactionResponse {.contract.} proc reserveSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256): Confirmable {.contract.}
proc canReserveSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256): bool {.contract, view.} proc canReserveSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256): bool {.contract, view.}

View File

@ -10,6 +10,7 @@
# This module implements serialization and deserialization of Manifest # This module implements serialization and deserialization of Manifest
import pkg/upraises import pkg/upraises
import times
push: {.upraises: [].} push: {.upraises: [].}
@ -59,6 +60,9 @@ proc encode*(manifest: Manifest): ?!seq[byte] =
# optional hcodec: MultiCodec = 5 # Multihash codec # optional hcodec: MultiCodec = 5 # Multihash codec
# optional version: CidVersion = 6; # Cid version # optional version: CidVersion = 6; # Cid version
# optional ErasureInfo erasure = 7; # erasure coding info # optional ErasureInfo erasure = 7; # erasure coding info
# optional filename: ?string = 8; # original filename
# optional mimetype: ?string = 9; # original mimetype
# optional uploadedAt: ?int64 = 10; # original uploadedAt
# } # }
# ``` # ```
# #
@ -70,6 +74,7 @@ proc encode*(manifest: Manifest): ?!seq[byte] =
header.write(4, manifest.codec.uint32) header.write(4, manifest.codec.uint32)
header.write(5, manifest.hcodec.uint32) header.write(5, manifest.hcodec.uint32)
header.write(6, manifest.version.uint32) header.write(6, manifest.version.uint32)
if manifest.protected: if manifest.protected:
var erasureInfo = initProtoBuffer() var erasureInfo = initProtoBuffer()
erasureInfo.write(1, manifest.ecK.uint32) erasureInfo.write(1, manifest.ecK.uint32)
@ -90,6 +95,15 @@ proc encode*(manifest: Manifest): ?!seq[byte] =
erasureInfo.finish() erasureInfo.finish()
header.write(7, erasureInfo) header.write(7, erasureInfo)
if manifest.filename.isSome:
header.write(8, manifest.filename.get())
if manifest.mimetype.isSome:
header.write(9, manifest.mimetype.get())
if manifest.uploadedAt.isSome:
header.write(10, manifest.uploadedAt.get().uint64)
pbNode.write(1, header) # set the treeCid as the data field pbNode.write(1, header) # set the treeCid as the data field
pbNode.finish() pbNode.finish()
@ -118,6 +132,9 @@ proc decode*(_: type Manifest, data: openArray[byte]): ?!Manifest =
slotRoots: seq[seq[byte]] slotRoots: seq[seq[byte]]
cellSize: uint32 cellSize: uint32
verifiableStrategy: uint32 verifiableStrategy: uint32
filename: string
mimetype: string
uploadedAt: uint64
# Decode `Header` message # Decode `Header` message
if pbNode.getField(1, pbHeader).isErr: if pbNode.getField(1, pbHeader).isErr:
@ -145,6 +162,15 @@ proc decode*(_: type Manifest, data: openArray[byte]): ?!Manifest =
if pbHeader.getField(7, pbErasureInfo).isErr: if pbHeader.getField(7, pbErasureInfo).isErr:
return failure("Unable to decode `erasureInfo` from manifest!") return failure("Unable to decode `erasureInfo` from manifest!")
if pbHeader.getField(8, filename).isErr:
return failure("Unable to decode `filename` from manifest!")
if pbHeader.getField(9, mimetype).isErr:
return failure("Unable to decode `mimetype` from manifest!")
if pbHeader.getField(10, uploadedAt).isErr:
return failure("Unable to decode `uploadedAt` from manifest!")
let protected = pbErasureInfo.buffer.len > 0 let protected = pbErasureInfo.buffer.len > 0
var verifiable = false var verifiable = false
if protected: if protected:
@ -183,6 +209,10 @@ proc decode*(_: type Manifest, data: openArray[byte]): ?!Manifest =
let let
treeCid = ? Cid.init(treeCidBuf).mapFailure treeCid = ? Cid.init(treeCidBuf).mapFailure
var filenameOption = if filename.len == 0: string.none else: filename.some
var mimetypeOption = if mimetype.len == 0: string.none else: mimetype.some
var uploadedAtOption = if uploadedAt == 0: int64.none else: uploadedAt.int64.some
let let
self = if protected: self = if protected:
Manifest.new( Manifest.new(
@ -196,7 +226,10 @@ proc decode*(_: type Manifest, data: openArray[byte]): ?!Manifest =
ecM = ecM.int, ecM = ecM.int,
originalTreeCid = ? Cid.init(originalTreeCid).mapFailure, originalTreeCid = ? Cid.init(originalTreeCid).mapFailure,
originalDatasetSize = originalDatasetSize.NBytes, originalDatasetSize = originalDatasetSize.NBytes,
strategy = StrategyType(protectedStrategy)) strategy = StrategyType(protectedStrategy),
filename = filenameOption,
mimetype = mimetypeOption,
uploadedAt = uploadedAtOption)
else: else:
Manifest.new( Manifest.new(
treeCid = treeCid, treeCid = treeCid,
@ -204,7 +237,10 @@ proc decode*(_: type Manifest, data: openArray[byte]): ?!Manifest =
blockSize = blockSize.NBytes, blockSize = blockSize.NBytes,
version = CidVersion(version), version = CidVersion(version),
hcodec = hcodec.MultiCodec, hcodec = hcodec.MultiCodec,
codec = codec.MultiCodec) codec = codec.MultiCodec,
filename = filenameOption,
mimetype = mimetypeOption,
uploadedAt = uploadedAtOption)
? self.verify() ? self.verify()

View File

@ -36,6 +36,9 @@ type
codec: MultiCodec # Dataset codec codec: MultiCodec # Dataset codec
hcodec: MultiCodec # Multihash codec hcodec: MultiCodec # Multihash codec
version: CidVersion # Cid version version: CidVersion # Cid version
filename {.serialize.}: ?string # The filename of the content uploaded (optional)
mimetype {.serialize.}: ?string # The mimetype of the content uploaded (optional)
uploadedAt {.serialize.}: ?int64 # The UTC creation timestamp in seconds
case protected {.serialize.}: 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
@ -121,6 +124,14 @@ func verifiableStrategy*(self: Manifest): StrategyType =
func numSlotBlocks*(self: Manifest): int = func numSlotBlocks*(self: Manifest): int =
divUp(self.blocksCount, self.numSlots) divUp(self.blocksCount, self.numSlots)
func filename*(self: Manifest): ?string =
self.filename
func mimetype*(self: Manifest): ?string =
self.mimetype
func uploadedAt*(self: Manifest): ?int64 =
self.uploadedAt
############################################################ ############################################################
# Operations on block list # Operations on block list
############################################################ ############################################################
@ -163,6 +174,9 @@ func `==`*(a, b: Manifest): bool =
(a.hcodec == b.hcodec) and (a.hcodec == b.hcodec) and
(a.codec == b.codec) and (a.codec == b.codec) and
(a.protected == b.protected) and (a.protected == b.protected) and
(a.filename == b.filename) and
(a.mimetype == b.mimetype) and
(a.uploadedAt == b.uploadedAt) and
(if a.protected: (if a.protected:
(a.ecK == b.ecK) and (a.ecK == b.ecK) and
(a.ecM == b.ecM) and (a.ecM == b.ecM) and
@ -181,14 +195,24 @@ func `==`*(a, b: Manifest): bool =
true) true)
func `$`*(self: Manifest): string = func `$`*(self: Manifest): string =
"treeCid: " & $self.treeCid & result = "treeCid: " & $self.treeCid &
", datasetSize: " & $self.datasetSize & ", datasetSize: " & $self.datasetSize &
", blockSize: " & $self.blockSize & ", blockSize: " & $self.blockSize &
", version: " & $self.version & ", version: " & $self.version &
", hcodec: " & $self.hcodec & ", hcodec: " & $self.hcodec &
", codec: " & $self.codec & ", codec: " & $self.codec &
", protected: " & $self.protected & ", protected: " & $self.protected
(if self.protected:
if self.filename.isSome:
result &= ", filename: " & $self.filename
if self.mimetype.isSome:
result &= ", mimetype: " & $self.mimetype
if self.uploadedAt.isSome:
result &= ", uploadedAt: " & $self.uploadedAt
result &= (if self.protected:
", ecK: " & $self.ecK & ", ecK: " & $self.ecK &
", ecM: " & $self.ecM & ", ecM: " & $self.ecM &
", originalTreeCid: " & $self.originalTreeCid & ", originalTreeCid: " & $self.originalTreeCid &
@ -202,6 +226,8 @@ func `$`*(self: Manifest): string =
else: else:
"") "")
return result
############################################################ ############################################################
# Constructors # Constructors
############################################################ ############################################################
@ -214,7 +240,10 @@ func new*(
version: CidVersion = CIDv1, version: CidVersion = CIDv1,
hcodec = Sha256HashCodec, hcodec = Sha256HashCodec,
codec = BlockCodec, codec = BlockCodec,
protected = false): Manifest = protected = false,
filename: ?string = string.none,
mimetype: ?string = string.none,
uploadedAt: ?int64 = int64.none): Manifest =
T( T(
treeCid: treeCid, treeCid: treeCid,
@ -223,7 +252,10 @@ func new*(
version: version, version: version,
codec: codec, codec: codec,
hcodec: hcodec, hcodec: hcodec,
protected: protected) protected: protected,
filename: filename,
mimetype: mimetype,
uploadedAt: uploadedAt)
func new*( func new*(
T: type Manifest, T: type Manifest,
@ -247,7 +279,11 @@ func new*(
ecK: ecK, ecM: ecM, ecK: ecK, ecM: ecM,
originalTreeCid: manifest.treeCid, originalTreeCid: manifest.treeCid,
originalDatasetSize: manifest.datasetSize, originalDatasetSize: manifest.datasetSize,
protectedStrategy: strategy) protectedStrategy: strategy,
filename: manifest.filename,
mimetype: manifest.mimetype,
uploadedAt: manifest.uploadedAt
)
func new*( func new*(
T: type Manifest, T: type Manifest,
@ -263,7 +299,10 @@ func new*(
codec: manifest.codec, codec: manifest.codec,
hcodec: manifest.hcodec, hcodec: manifest.hcodec,
blockSize: manifest.blockSize, blockSize: manifest.blockSize,
protected: false) protected: false,
filename: manifest.filename,
mimetype: manifest.mimetype,
uploadedAt: manifest.uploadedAt)
func new*( func new*(
T: type Manifest, T: type Manifest,
@ -277,7 +316,10 @@ func new*(
ecM: int, ecM: int,
originalTreeCid: Cid, originalTreeCid: Cid,
originalDatasetSize: NBytes, originalDatasetSize: NBytes,
strategy = SteppedStrategy): Manifest = strategy = SteppedStrategy,
filename: ?string = string.none,
mimetype: ?string = string.none,
uploadedAt: ?int64 = int64.none): Manifest =
Manifest( Manifest(
treeCid: treeCid, treeCid: treeCid,
@ -291,7 +333,10 @@ func new*(
ecM: ecM, ecM: ecM,
originalTreeCid: originalTreeCid, originalTreeCid: originalTreeCid,
originalDatasetSize: originalDatasetSize, originalDatasetSize: originalDatasetSize,
protectedStrategy: strategy) protectedStrategy: strategy,
filename: filename,
mimetype: mimetype,
uploadedAt: uploadedAt)
func new*( func new*(
T: type Manifest, T: type Manifest,
@ -329,7 +374,11 @@ func new*(
verifyRoot: verifyRoot, verifyRoot: verifyRoot,
slotRoots: @slotRoots, slotRoots: @slotRoots,
cellSize: cellSize, cellSize: cellSize,
verifiableStrategy: strategy) verifiableStrategy: strategy,
filename: manifest.filename,
mimetype: manifest.mimetype,
uploadedAt: manifest.uploadedAt
)
func new*( func new*(
T: type Manifest, T: type Manifest,

View File

@ -14,6 +14,7 @@ import std/sequtils
import std/strformat import std/strformat
import std/sugar import std/sugar
import std/cpuinfo import std/cpuinfo
import times
import pkg/questionable import pkg/questionable
import pkg/questionable/results import pkg/questionable/results
@ -297,6 +298,8 @@ proc retrieve*(
proc store*( proc store*(
self: CodexNodeRef, self: CodexNodeRef,
stream: LPStream, stream: LPStream,
filename: ?string = string.none,
mimetype: ?string = string.none,
blockSize = DefaultBlockSize): Future[?!Cid] {.async.} = blockSize = DefaultBlockSize): Future[?!Cid] {.async.} =
## Save stream contents as dataset with given blockSize ## Save stream contents as dataset with given blockSize
## to nodes's BlockStore, and return Cid of its manifest ## to nodes's BlockStore, and return Cid of its manifest
@ -355,7 +358,10 @@ proc store*(
datasetSize = NBytes(chunker.offset), datasetSize = NBytes(chunker.offset),
version = CIDv1, version = CIDv1,
hcodec = hcodec, hcodec = hcodec,
codec = dataCodec) codec = dataCodec,
filename = filename,
mimetype = mimetype,
uploadedAt = now().utc.toTime.toUnix.some)
without manifestBlk =? await self.storeManifest(manifest), err: without manifestBlk =? await self.storeManifest(manifest), err:
error "Unable to store manifest" error "Unable to store manifest"
@ -364,7 +370,9 @@ proc store*(
info "Stored data", manifestCid = manifestBlk.cid, info "Stored data", manifestCid = manifestBlk.cid,
treeCid = treeCid, treeCid = treeCid,
blocks = manifest.blocksCount, blocks = manifest.blocksCount,
datasetSize = manifest.datasetSize datasetSize = manifest.datasetSize,
filename = manifest.filename,
mimetype = manifest.mimetype
return manifestBlk.cid.success return manifestBlk.cid.success
@ -749,15 +757,15 @@ proc stop*(self: CodexNodeRef) {.async.} =
if not self.discovery.isNil: if not self.discovery.isNil:
await self.discovery.stop() await self.discovery.stop()
if not self.clock.isNil:
await self.clock.stop()
if clientContracts =? self.contracts.client: if clientContracts =? self.contracts.client:
await clientContracts.stop() await clientContracts.stop()
if hostContracts =? self.contracts.host: if hostContracts =? self.contracts.host:
await hostContracts.stop() await hostContracts.stop()
if not self.clock.isNil:
await self.clock.stop()
if validatorContracts =? self.contracts.validator: if validatorContracts =? self.contracts.validator:
await validatorContracts.stop() await validatorContracts.stop()

View File

@ -13,6 +13,8 @@ push: {.upraises: [].}
import std/sequtils import std/sequtils
import mimetypes
import os
import pkg/questionable import pkg/questionable
import pkg/questionable/results import pkg/questionable/results
@ -81,11 +83,27 @@ proc retrieveCid(
try: try:
without stream =? (await node.retrieve(cid, local)), error: without stream =? (await node.retrieve(cid, local)), error:
if error of BlockNotFoundError: if error of BlockNotFoundError:
return RestApiResponse.error(Http404, error.msg) resp.status = Http404
return await resp.sendBody("")
else: else:
return RestApiResponse.error(Http500, error.msg) resp.status = Http500
return await resp.sendBody(error.msg)
# It is ok to fetch again the manifest because it will hit the cache
without manifest =? (await node.fetchManifest(cid)), err:
error "Failed to fetch manifest", err = err.msg
resp.status = Http404
return await resp.sendBody(err.msg)
if manifest.mimetype.isSome:
resp.setHeader("Content-Type", manifest.mimetype.get())
else:
resp.addHeader("Content-Type", "application/octet-stream") resp.addHeader("Content-Type", "application/octet-stream")
if manifest.filename.isSome:
resp.setHeader("Content-Disposition", "attachment; filename=\"" & manifest.filename.get() & "\"")
await resp.prepareChunked() await resp.prepareChunked()
while not stream.atEof: while not stream.atEof:
@ -98,12 +116,14 @@ proc retrieveCid(
break break
bytes += buff.len bytes += buff.len
await resp.sendChunk(addr buff[0], buff.len) await resp.sendChunk(addr buff[0], buff.len)
await resp.finish() await resp.finish()
codex_api_downloads.inc() codex_api_downloads.inc()
except CatchableError as exc: except CatchableError as exc:
warn "Excepting streaming blocks", exc = exc.msg warn "Excepting streaming blocks", exc = exc.msg
return RestApiResponse.error(Http500) resp.status = Http500
return await resp.sendBody("")
finally: finally:
info "Sent bytes", cid = cid, bytes info "Sent bytes", cid = cid, bytes
if not stream.isNil: if not stream.isNil:
@ -124,6 +144,18 @@ proc setCorsHeaders(resp: HttpResponseRef, httpMethod: string, origin: string) =
resp.setHeader("Access-Control-Allow-Methods", httpMethod & ", OPTIONS") resp.setHeader("Access-Control-Allow-Methods", httpMethod & ", OPTIONS")
resp.setHeader("Access-Control-Max-Age", "86400") resp.setHeader("Access-Control-Max-Age", "86400")
proc getFilenameFromContentDisposition(contentDisposition: string): ?string =
if not("filename=" in contentDisposition):
return string.none
let parts = contentDisposition.split("filename=\"")
if parts.len < 2:
return string.none
let filename = parts[1].strip()
return filename[0..^2].some
proc initDataApi(node: CodexNodeRef, repoStore: RepoStore, router: var RestRouter) = proc initDataApi(node: CodexNodeRef, repoStore: RepoStore, router: var RestRouter) =
let allowedOrigin = router.allowedOrigin # prevents capture inside of api defintion let allowedOrigin = router.allowedOrigin # prevents capture inside of api defintion
@ -134,7 +166,7 @@ proc initDataApi(node: CodexNodeRef, repoStore: RepoStore, router: var RestRoute
if corsOrigin =? allowedOrigin: if corsOrigin =? allowedOrigin:
resp.setCorsHeaders("POST", corsOrigin) resp.setCorsHeaders("POST", corsOrigin)
resp.setHeader("Access-Control-Allow-Headers", "content-type") resp.setHeader("Access-Control-Allow-Headers", "content-type, content-disposition")
resp.status = Http204 resp.status = Http204
await resp.sendBody("") await resp.sendBody("")
@ -157,12 +189,31 @@ proc initDataApi(node: CodexNodeRef, repoStore: RepoStore, router: var RestRoute
# #
await request.handleExpect() await request.handleExpect()
var mimetype = request.headers.getString(ContentTypeHeader).some
if mimetype.get() != "":
var m = newMimetypes()
let extension = m.getExt(mimetype.get(), "")
if extension == "":
return RestApiResponse.error(Http422, "The MIME type is not valid.")
else:
mimetype = string.none
const ContentDispositionHeader = "Content-Disposition"
let contentDisposition = request.headers.getString(ContentDispositionHeader)
let filename = getFilenameFromContentDisposition(contentDisposition)
if filename.isSome and not isValidFilename(filename.get()):
return RestApiResponse.error(Http422, "The filename is not valid.")
# Here we could check if the extension matches the filename if needed
let let
reader = bodyReader.get() reader = bodyReader.get()
try: try:
without cid =? ( without cid =? (
await node.store(AsyncStreamWrapper.new(reader = AsyncStreamReader(reader)))), error: await node.store(AsyncStreamWrapper.new(reader = AsyncStreamReader(reader)), filename = filename, mimetype = mimetype)), error:
error "Error uploading file", exc = error.msg error "Error uploading file", exc = error.msg
return RestApiResponse.error(Http500, error.msg) return RestApiResponse.error(Http500, error.msg)
@ -276,10 +327,6 @@ proc initDataApi(node: CodexNodeRef, repoStore: RepoStore, router: var RestRoute
Http400, Http400,
$cid.error(), headers = headers) $cid.error(), headers = headers)
if corsOrigin =? allowedOrigin:
resp.setCorsHeaders("GET", corsOrigin)
resp.setHeader("Access-Control-Headers", "X-Requested-With")
without manifest =? (await node.fetchManifest(cid.get())), err: without manifest =? (await node.fetchManifest(cid.get())), err:
error "Failed to fetch manifest", err = err.msg error "Failed to fetch manifest", err = err.msg
return RestApiResponse.error( return RestApiResponse.error(

View File

@ -59,19 +59,16 @@ proc onError(machine: Machine, error: ref CatchableError): Event =
state.onError(error) state.onError(error)
proc run(machine: Machine, state: State) {.async.} = proc run(machine: Machine, state: State) {.async.} =
try:
if next =? await state.run(machine): if next =? await state.run(machine):
machine.schedule(Event.transition(state, next)) machine.schedule(Event.transition(state, next))
except CancelledError:
discard
proc scheduler(machine: Machine) {.async.} = proc scheduler(machine: Machine) {.async.} =
var running: Future[void] var running: Future[void]
try:
while machine.started: while machine.started:
let event = await machine.scheduled.get().track(machine) let event = await machine.scheduled.get().track(machine)
if next =? event(machine.state): if next =? event(machine.state):
if not running.isNil and not running.finished: if not running.isNil and not running.finished:
trace "cancelling current state", state = $machine.state
await running.cancelAndWait() await running.cancelAndWait()
let fromState = if machine.state.isNil: "<none>" else: $machine.state let fromState = if machine.state.isNil: "<none>" else: $machine.state
machine.state = next machine.state = next
@ -79,11 +76,11 @@ proc scheduler(machine: Machine) {.async.} =
running = machine.run(machine.state) running = machine.run(machine.state)
running running
.track(machine) .track(machine)
.catch((err: ref CatchableError) => .cancelled(proc() = trace "state.run cancelled, swallowing", state = $machine.state)
.catch(proc(err: ref CatchableError) =
trace "error caught in state.run, calling state.onError", state = $machine.state
machine.schedule(machine.onError(err)) machine.schedule(machine.onError(err))
) )
except CancelledError:
discard
proc start*(machine: Machine, initialState: State) = proc start*(machine: Machine, initialState: State) =
if machine.started: if machine.started:
@ -93,12 +90,13 @@ proc start*(machine: Machine, initialState: State) =
machine.scheduled = newAsyncQueue[Event]() machine.scheduled = newAsyncQueue[Event]()
machine.started = true machine.started = true
machine.scheduler() try:
.track(machine) discard machine.scheduler().track(machine)
.catch((err: ref CatchableError) =>
error("Error in scheduler", error = err.msg)
)
machine.schedule(Event.transition(machine.state, initialState)) machine.schedule(Event.transition(machine.state, initialState))
except CancelledError as e:
discard
except CatchableError as e:
error("Error in scheduler", error = e.msg)
proc stop*(machine: Machine) {.async.} = proc stop*(machine: Machine) {.async.} =
if not machine.started: if not machine.started:

View File

@ -357,6 +357,19 @@ components:
protected: protected:
type: boolean type: boolean
description: "Indicates if content is protected by erasure-coding" description: "Indicates if content is protected by erasure-coding"
filename:
type: string
description: "The original name of the uploaded content (optional)"
example: codex.png
mimetype:
type: string
description: "The original mimetype of the uploaded content (optional)"
example: image/png
uploadedAt:
type: integer
format: int64
description: "The UTC upload timestamp in seconds"
example: 1729244192
Space: Space:
type: object type: object
@ -442,12 +455,29 @@ paths:
description: Invalid CID is specified description: Invalid CID is specified
"404": "404":
description: Content specified by the CID is not found description: Content specified by the CID is not found
"422":
description: The content type is not a valid content type or the filename is not valid
"500": "500":
description: Well it was bad-bad description: Well it was bad-bad
post: 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." 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 ] tags: [ Data ]
operationId: upload operationId: upload
parameters:
- name: content-type
in: header
required: false
description: The content type of the file. Must be valid.
schema:
type: string
example: "image/png"
- name: content-disposition
in: header
required: false
description: The content disposition used to send the filename.
schema:
type: string
example: "attachment; filename=\"codex.png\""
requestBody: requestBody:
content: content:
application/octet-stream: application/octet-stream:

View File

@ -83,6 +83,8 @@ suite "Manifest - Attribute Inheritance":
treeCid = Cid.example, treeCid = Cid.example,
blockSize = 1.MiBs, blockSize = 1.MiBs,
datasetSize = 100.MiBs, datasetSize = 100.MiBs,
filename = "codex.png".some,
mimetype = "image/png".some
), ),
treeCid = Cid.example, treeCid = Cid.example,
datasetSize = 200.MiBs, datasetSize = 200.MiBs,
@ -107,3 +109,15 @@ suite "Manifest - Attribute Inheritance":
).tryGet() ).tryGet()
check verifiable.protectedStrategy == LinearStrategy check verifiable.protectedStrategy == LinearStrategy
test "Should preserve metadata for manifest in verifiable manifest":
var verifiable = Manifest.new(
manifest = makeProtectedManifest(SteppedStrategy),
verifyRoot = Cid.example,
slotRoots = @[Cid.example, Cid.example]
).tryGet()
check verifiable.filename.isSome == true
check verifiable.filename.get() == "codex.png"
check verifiable.mimetype.isSome == true
check verifiable.mimetype.get() == "image/png"

View File

@ -38,7 +38,7 @@ ethersuite "Marketplace contracts":
let tokenAddress = await marketplace.token() let tokenAddress = await marketplace.token()
token = Erc20Token.new(tokenAddress, ethProvider.getSigner()) token = Erc20Token.new(tokenAddress, ethProvider.getSigner())
let config = await marketplace.config() let config = await marketplace.configuration()
periodicity = Periodicity(seconds: config.proofs.period) periodicity = Periodicity(seconds: config.proofs.period)
request = StorageRequest.example request = StorageRequest.example

View File

@ -9,7 +9,7 @@ import ../checktest
type MockProvider = ref object of Provider type MockProvider = ref object of Provider
chainId*: UInt256 chainId*: UInt256
method getChainId*(provider: MockProvider): Future[UInt256] {.async.} = method getChainId*(provider: MockProvider): Future[UInt256] {.async: (raises:[ProviderError]).} =
return provider.chainId return provider.chainId
proc configFactory(): CodexConf = proc configFactory(): CodexConf =

View File

@ -34,7 +34,7 @@ ethersuite "On-Chain Market":
setup: setup:
let address = Marketplace.address(dummyVerifier = true) let address = Marketplace.address(dummyVerifier = true)
marketplace = Marketplace.new(address, ethProvider.getSigner()) marketplace = Marketplace.new(address, ethProvider.getSigner())
let config = await marketplace.config() let config = await marketplace.configuration()
hostRewardRecipient = accounts[2] hostRewardRecipient = accounts[2]
market = OnChainMarket.new(marketplace) market = OnChainMarket.new(marketplace)
@ -76,13 +76,13 @@ ethersuite "On-Chain Market":
test "can retrieve proof periodicity": test "can retrieve proof periodicity":
let periodicity = await market.periodicity() let periodicity = await market.periodicity()
let config = await marketplace.config() let config = await marketplace.configuration()
let periodLength = config.proofs.period let periodLength = config.proofs.period
check periodicity.seconds == periodLength check periodicity.seconds == periodLength
test "can retrieve proof timeout": test "can retrieve proof timeout":
let proofTimeout = await market.proofTimeout() let proofTimeout = await market.proofTimeout()
let config = await marketplace.config() let config = await marketplace.configuration()
check proofTimeout == config.proofs.timeout check proofTimeout == config.proofs.timeout
test "supports marketplace requests": test "supports marketplace requests":

View File

@ -1,4 +1,5 @@
import pkg/ethers import pkg/ethers
import pkg/serde/json
proc currentTime*(provider: Provider): Future[UInt256] {.async.} = proc currentTime*(provider: Provider): Future[UInt256] {.async.} =
return (!await provider.getBlock(BlockTag.pending)).timestamp return (!await provider.getBlock(BlockTag.pending)).timestamp

View File

@ -257,3 +257,13 @@ proc saleStateIs*(client: CodexClient, id: SlotId, state: string): bool =
proc requestId*(client: CodexClient, id: PurchaseId): ?RequestId = proc requestId*(client: CodexClient, id: PurchaseId): ?RequestId =
return client.getPurchase(id).option.?requestId return client.getPurchase(id).option.?requestId
proc uploadRaw*(client: CodexClient, contents: string, headers = newHttpHeaders()): Response =
return client.http.request(client.baseurl & "/data", body = contents, httpMethod=HttpPost, headers = headers)
proc listRaw*(client: CodexClient): Response =
return client.http.request(client.baseurl & "/data", httpMethod=HttpGet)
proc downloadRaw*(client: CodexClient, cid: string, local = false): Response =
return client.http.request(client.baseurl & "/data/" & cid &
(if local: "" else: "/network/stream"), httpMethod=HttpGet)

View File

@ -85,7 +85,7 @@ template marketplacesuite*(name: string, body: untyped) =
marketplace = Marketplace.new(Marketplace.address, ethProvider.getSigner()) marketplace = Marketplace.new(Marketplace.address, ethProvider.getSigner())
let tokenAddress = await marketplace.token() let tokenAddress = await marketplace.token()
token = Erc20Token.new(tokenAddress, ethProvider.getSigner()) token = Erc20Token.new(tokenAddress, ethProvider.getSigner())
let config = await mp.config(marketplace) let config = await marketplace.configuration()
period = config.proofs.period.truncate(uint64) period = config.proofs.period.truncate(uint64)
periodicity = Periodicity(seconds: period.u256) periodicity = Periodicity(seconds: period.u256)

View File

@ -4,9 +4,9 @@ from pkg/libp2p import `==`
import pkg/codex/units import pkg/codex/units
import ./twonodes import ./twonodes
import ../examples import ../examples
import json
twonodessuite "REST API", debug1 = false, debug2 = false: twonodessuite "REST API", debug1 = false, debug2 = false:
test "nodes can print their peer information": test "nodes can print their peer information":
check !client1.info() != !client2.info() check !client1.info() != !client2.info()
@ -16,6 +16,7 @@ twonodessuite "REST API", debug1 = false, debug2 = false:
test "node accepts file uploads": test "node accepts file uploads":
let cid1 = client1.upload("some file contents").get let cid1 = client1.upload("some file contents").get
let cid2 = client1.upload("some other contents").get let cid2 = client1.upload("some other contents").get
check cid1 != cid2 check cid1 != cid2
test "node shows used and available space": test "node shows used and available space":
@ -25,7 +26,7 @@ twonodessuite "REST API", debug1 = false, debug2 = false:
check: check:
space.totalBlocks == 2 space.totalBlocks == 2
space.quotaMaxBytes == 8589934592.NBytes space.quotaMaxBytes == 8589934592.NBytes
space.quotaUsedBytes == 65592.NBytes space.quotaUsedBytes == 65598.NBytes
space.quotaReservedBytes == 12.NBytes space.quotaReservedBytes == 12.NBytes
test "node lists local files": test "node lists local files":
@ -151,3 +152,89 @@ twonodessuite "REST API", debug1 = false, debug2 = false:
tolerance.uint) tolerance.uint)
check responseBefore.status == "200 OK" check responseBefore.status == "200 OK"
test "node accepts file uploads with content type":
let headers = newHttpHeaders({"Content-Type": "text/plain"})
let response = client1.uploadRaw("some file contents", headers)
check response.status == "200 OK"
check response.body != ""
test "node accepts file uploads with content disposition":
let headers = newHttpHeaders({"Content-Disposition": "attachment; filename=\"example.txt\""})
let response = client1.uploadRaw("some file contents", headers)
check response.status == "200 OK"
check response.body != ""
test "node accepts file uploads with content disposition without filename":
let headers = newHttpHeaders({"Content-Disposition": "attachment"})
let response = client1.uploadRaw("some file contents", headers)
check response.status == "200 OK"
check response.body != ""
test "upload fails if content disposition contains bad filename":
let headers = newHttpHeaders({"Content-Disposition": "attachment; filename=\"exam*ple.txt\""})
let response = client1.uploadRaw("some file contents", headers)
check response.status == "422 Unprocessable Entity"
check response.body == "The filename is not valid."
test "upload fails if content type is invalid":
let headers = newHttpHeaders({"Content-Type": "hello/world"})
let response = client1.uploadRaw("some file contents", headers)
check response.status == "422 Unprocessable Entity"
check response.body == "The MIME type is not valid."
test "node retrieve the metadata":
let headers = newHttpHeaders({"Content-Type": "text/plain", "Content-Disposition": "attachment; filename=\"example.txt\""})
let uploadResponse = client1.uploadRaw("some file contents", headers)
let cid = uploadResponse.body
let listResponse = client1.listRaw()
let jsonData = parseJson(listResponse.body)
check jsonData.hasKey("content") == true
let content = jsonData["content"][0]
check content.hasKey("manifest") == true
let manifest = content["manifest"]
check manifest.hasKey("filename") == true
check manifest["filename"].getStr() == "example.txt"
check manifest.hasKey("mimetype") == true
check manifest["mimetype"].getStr() == "text/plain"
check manifest.hasKey("uploadedAt") == true
check manifest["uploadedAt"].getInt() > 0
test "node set the headers when for download":
let headers = newHttpHeaders({
"Content-Disposition": "attachment; filename=\"example.txt\"",
"Content-Type": "text/plain"
})
let uploadResponse = client1.uploadRaw("some file contents", headers)
let cid = uploadResponse.body
check uploadResponse.status == "200 OK"
let response = client1.downloadRaw(cid)
check response.status == "200 OK"
check response.headers.hasKey("Content-Type") == true
check response.headers["Content-Type"] == "text/plain"
check response.headers.hasKey("Content-Disposition") == true
check response.headers["Content-Disposition"] == "attachment; filename=\"example.txt\""
let local = true
let localResponse = client1.downloadRaw(cid, local)
check localResponse.status == "200 OK"
check localResponse.headers.hasKey("Content-Type") == true
check localResponse.headers["Content-Type"] == "text/plain"
check localResponse.headers.hasKey("Content-Disposition") == true
check localResponse.headers["Content-Disposition"] == "attachment; filename=\"example.txt\""

View File

@ -1,5 +1,7 @@
import pkg/codex/rest/json import pkg/codex/rest/json
import ./twonodes import ./twonodes
import json
from pkg/libp2p import Cid, `$`
twonodessuite "Uploads and downloads", debug1 = false, debug2 = false: twonodessuite "Uploads and downloads", debug1 = false, debug2 = false:
@ -39,24 +41,42 @@ twonodessuite "Uploads and downloads", debug1 = false, debug2 = false:
check: check:
resp2.error.msg == "404 Not Found" resp2.error.msg == "404 Not Found"
proc checkRestContent(content: ?!string) = proc checkRestContent(cid: Cid, content: ?!string) =
let c = content.tryGet() let c = content.tryGet()
# tried to JSON (very easy) and checking the resulting object (would be much nicer) # tried to JSON (very easy) and checking the resulting object (would be much nicer)
# spent an hour to try and make it work. # spent an hour to try and make it work.
check: let jsonData = parseJson(c)
c == "{\"cid\":\"zDvZRwzm1ePSzKSXt57D5YxHwcSDmsCyYN65wW4HT7fuX9HrzFXy\",\"manifest\":{\"treeCid\":\"zDzSvJTezk7bJNQqFq8k1iHXY84psNuUfZVusA5bBQQUSuyzDSVL\",\"datasetSize\":18,\"blockSize\":65536,\"protected\":false}}"
check jsonData.hasKey("cid") == true
check jsonData["cid"].getStr() == $cid
check jsonData.hasKey("manifest") == true
let manifest = jsonData["manifest"]
check manifest.hasKey("treeCid") == true
check manifest["treeCid"].getStr() == "zDzSvJTezk7bJNQqFq8k1iHXY84psNuUfZVusA5bBQQUSuyzDSVL"
check manifest.hasKey("datasetSize") == true
check manifest["datasetSize"].getInt() == 18
check manifest.hasKey("blockSize") == true
check manifest["blockSize"].getInt() == 65536
check manifest.hasKey("protected") == true
check manifest["protected"].getBool() == false
test "node allows downloading only manifest": test "node allows downloading only manifest":
let content1 = "some file contents" let content1 = "some file contents"
let cid1 = client1.upload(content1).get let cid1 = client1.upload(content1).get
let resp2 = client2.downloadManifestOnly(cid1)
checkRestContent(resp2) let resp2 = client1.downloadManifestOnly(cid1)
checkRestContent(cid1, resp2)
test "node allows downloading content without stream": test "node allows downloading content without stream":
let content1 = "some file contents" let content1 = "some file contents"
let cid1 = client1.upload(content1).get let cid1 = client1.upload(content1).get
let resp1 = client2.downloadNoStream(cid1) let resp1 = client2.downloadNoStream(cid1)
checkRestContent(resp1) checkRestContent(cid1, resp1)
let resp2 = client2.download(cid1, local = true).get let resp2 = client2.download(cid1, local = true).get
check: check:
content1 == resp2 content1 == resp2

View File

@ -2,10 +2,11 @@ import std/os
import std/osproc import std/osproc
import std/options import std/options
import pkg/chronos import pkg/chronos
import codex/contracts import pkg/codex/contracts
import ../../integration/marketplacesuite import ../../asynctest
import ../../contracts/deployment
marketplacesuite "tools/cirdl": suite "tools/cirdl":
const const
cirdl = "build" / "cirdl" cirdl = "build" / "cirdl"
workdir = "." workdir = "."
@ -14,11 +15,11 @@ marketplacesuite "tools/cirdl":
let let
circuitPath = "testcircuitpath" circuitPath = "testcircuitpath"
rpcEndpoint = "ws://localhost:8545" rpcEndpoint = "ws://localhost:8545"
marketplaceAddress = $marketplace.address marketplaceAddress = Marketplace.address
discard existsOrCreateDir(circuitPath) discard existsOrCreateDir(circuitPath)
let args = [circuitPath, rpcEndpoint, marketplaceAddress] let args = [circuitPath, rpcEndpoint, $marketplaceAddress]
let process = osproc.startProcess( let process = osproc.startProcess(
cirdl, cirdl,

View File

@ -34,7 +34,7 @@ proc getCircuitHash(rpcEndpoint: string, marketplaceAddress: string): Future[?!s
return failure("Invalid address: " & marketplaceAddress) return failure("Invalid address: " & marketplaceAddress)
let marketplace = Marketplace.new(address, provider) let marketplace = Marketplace.new(address, provider)
let config = await marketplace.config() let config = await marketplace.configuration()
return success config.proofs.zkeyHash return success config.proofs.zkeyHash
proc formatUrl(hash: string): string = proc formatUrl(hash: string): string =

@ -1 +1 @@
Subproject commit 997696a20e0976011cdbc2f0ff3a844672056ba2 Subproject commit 1ce3d10fa2ed325f4f4d1608434e5c6bd666ce24

2
vendor/nim-ethers vendored

@ -1 +1 @@
Subproject commit 5b170adcb1ffb1dbb273d1b7679bf3d9a08adb76 Subproject commit 6523e70eafecc3b9728a491058818e685bd384fb

2
vendor/nim-json-rpc vendored

@ -1 +1 @@
Subproject commit 0bf2bcbe74a18a3c7a709d57108bb7b51e748a92 Subproject commit 0408795be95c00d75e96eaef6eae8a9c734014f5

@ -1 +1 @@
Subproject commit bb53d49caf2a6c6cf1df365ba84af93cdcfa7aa3 Subproject commit 5127b26ee58076e9369e7c126c196793c2b12e73

2
vendor/nim-serde vendored

@ -1 +1 @@
Subproject commit b1e5e5d39a99ea56b750f6d9272dd319f4ad4291 Subproject commit 83e4a2ccf621d3040c6e7e0267393ca2d205988e

@ -1 +1 @@
Subproject commit 384eb2561ee755446cff512a8e057325848b86a7 Subproject commit f709bd9e16b1b6870fe3e4401196479e014a2ef6