mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-01-06 15:33:06 +00:00
adds torrent parser to support native torrent files when downloading
This commit is contained in:
parent
03ff25bbb1
commit
0f62624613
41
codex/bittorrent/torrentparser.nim
Normal file
41
codex/bittorrent/torrentparser.nim
Normal file
@ -0,0 +1,41 @@
|
||||
import std/strutils
|
||||
import std/re
|
||||
|
||||
import pkg/questionable/results
|
||||
import pkg/stew/byteutils
|
||||
import pkg/stew/base10
|
||||
|
||||
import ../errors
|
||||
|
||||
proc extractInfoFromTorrent*(torrentBytes: seq[byte]): ?!seq[byte] =
|
||||
## Extract the info from a torrent file
|
||||
##
|
||||
## params:
|
||||
## torrentBytes: the torrent file bytes
|
||||
##
|
||||
## returns: the bytes containing only the content of the info dictionary
|
||||
## or a failure if info is not found or invalid
|
||||
let torrentStr = string.fromBytes(torrentBytes)
|
||||
if torrentStr.contains("file tree") or torrentStr.contains("piece layers"):
|
||||
return failure("Torrent v2 provided. Only v1 is currently supported.")
|
||||
let infoKeyPos = torrentStr.find("info")
|
||||
if infoKeyPos == -1:
|
||||
return failure("Torrent file does not contain info dictionary.")
|
||||
let infoStartPos = infoKeyPos + "info".len
|
||||
if torrentStr[infoStartPos] != 'd':
|
||||
return failure("Torrent file does not contain valid info dictionary.")
|
||||
|
||||
var matches = newSeq[tuple[first, last: int]](1)
|
||||
let (_, piecesEndIndex) = torrentStr.findBounds(re"pieces(\d+):", matches)
|
||||
if matches.len == 1:
|
||||
let (first, last) = matches[0]
|
||||
let piecesLenStr = torrentStr[first .. last]
|
||||
without piecesLen =? Base10.decode(uint, piecesLenStr).mapFailure, err:
|
||||
return failure("Error decoding pieces length: " & err.msg)
|
||||
let piecesEndMarkerPos = piecesEndIndex + 1 + piecesLen.int
|
||||
if torrentStr[piecesEndMarkerPos] != 'e':
|
||||
return failure("Torrent file does not contain valid pieces.")
|
||||
let infoDirStr = torrentStr[infoStartPos .. piecesEndMarkerPos]
|
||||
infoDirStr.toBytes().success
|
||||
else:
|
||||
return failure("Torrent file does not contain valid pieces.")
|
||||
@ -43,6 +43,7 @@ import ../utils/options
|
||||
import ../bittorrent/manifest
|
||||
import ../bittorrent/torrentdownloader
|
||||
import ../bittorrent/magnetlink
|
||||
import ../bittorrent/torrentparser
|
||||
|
||||
import ../tarballs/[directorymanifest, directorydownloader, tarballnodeextensions]
|
||||
|
||||
@ -681,6 +682,57 @@ proc initDataApi(node: CodexNodeRef, repoStore: RepoStore, router: var RestRoute
|
||||
Http422, "The magnet link does not contain a valid info hash."
|
||||
)
|
||||
await node.retrieveInfoHash(infoHash, resp = resp)
|
||||
# return
|
||||
# RestApiResponse.response($magnetLink, Http200, "text/plain")
|
||||
|
||||
router.api(MethodOptions, "/api/codex/v1/torrent/torrent-file") do(
|
||||
resp: HttpResponseRef
|
||||
) -> RestApiResponse:
|
||||
if corsOrigin =? allowedOrigin:
|
||||
resp.setCorsHeaders("POST", corsOrigin)
|
||||
resp.setHeader(
|
||||
"Access-Control-Allow-Headers", "content-type, content-disposition"
|
||||
)
|
||||
|
||||
resp.status = Http204
|
||||
await resp.sendBody("")
|
||||
|
||||
router.api(MethodPost, "/api/codex/v1/torrent/torrent-file") do(
|
||||
contentBody: Option[ContentBody], resp: HttpResponseRef
|
||||
) -> RestApiResponse:
|
||||
let mimeType = request.headers.getString(ContentTypeHeader)
|
||||
echo "mimeType: ", mimeType
|
||||
if mimeType != "application/json" and mimeType != "application/octet-stream":
|
||||
return RestApiResponse.error(
|
||||
Http422,
|
||||
"Missing \"Content-Type\" header: expected either \"application/json\" or \"application/octet-stream\".",
|
||||
)
|
||||
without torrentBytes =? contentBody .? data:
|
||||
return RestApiResponse.error(Http422, "No torrent file content provided.")
|
||||
var infoBytes: seq[byte]
|
||||
if mimeType == "application/json":
|
||||
without torrentManifest =? BitTorrentManifest.fromJson(torrentBytes):
|
||||
return RestApiResponse.error(Http422, "Invalid torrent JSON file content.")
|
||||
echo "torrentManifest: ", torrentManifest
|
||||
let torrentInfo = torrentManifest.info
|
||||
# very basic validation for now
|
||||
if torrentInfo.length == 0 or torrentInfo.pieceLength == 0 or
|
||||
torrentInfo.pieces.len == 0:
|
||||
return
|
||||
RestApiResponse.error(Http422, "The torrent file is invalid or incomplete.")
|
||||
# return RestApiResponse.response($torrentInfo, contentType = "text/plain")
|
||||
infoBytes = bencode(torrentInfo)
|
||||
else:
|
||||
without infoBencoded =? extractInfoFromTorrent(torrentBytes), err:
|
||||
return RestApiResponse.error(
|
||||
Http422, "Failed extracting info directory from the torrent file."
|
||||
)
|
||||
infoBytes = infoBencoded
|
||||
without infoHash =? MultiHash.digest($Sha1HashCodec, infoBytes).mapFailure, err:
|
||||
return RestApiResponse.error(
|
||||
Http422, "The torrent file does not contain a valid info hash."
|
||||
)
|
||||
return await node.retrieveInfoHash(infoHash, resp = resp)
|
||||
|
||||
router.api(MethodGet, "/api/codex/v1/torrent/{infoHash}/network/stream") do(
|
||||
infoHash: MultiHash, resp: HttpResponseRef
|
||||
|
||||
48
tests/codex/bittorrent/testtorrentparser.nim
Normal file
48
tests/codex/bittorrent/testtorrentparser.nim
Normal file
@ -0,0 +1,48 @@
|
||||
import std/sequtils
|
||||
|
||||
import pkg/unittest2
|
||||
|
||||
import pkg/libp2p/[multicodec, multihash]
|
||||
import pkg/questionable/results
|
||||
import pkg/stew/byteutils
|
||||
|
||||
import ../examples
|
||||
|
||||
import pkg/codex/bittorrent/manifest/manifest
|
||||
import pkg/codex/bittorrent/torrentParser
|
||||
|
||||
suite "torrentParser":
|
||||
test "extracts info directory bytes from the torrent binary data":
|
||||
let pieces = @[
|
||||
"21FEBA308CD51E9ACF88417193A9EA60F0F84646",
|
||||
"3D4A8279853DA2DA355A574740217D446506E8EB",
|
||||
"1AD686B48B9560B15B8843FD00E7EC1B59624B09",
|
||||
"5015E7DA0C40350624C6B5A1FED1DB39720B726C",
|
||||
].map(
|
||||
proc(hash: string): MultiHash =
|
||||
let bytes = hash.hexToSeqByte.catch.tryGet()
|
||||
MultiHash.init($Sha1HashCodec, bytes).mapFailure.tryGet()
|
||||
)
|
||||
|
||||
let info = BitTorrentInfo(
|
||||
length: 1048576, pieceLength: 262144, pieces: pieces, name: some("data1M.bin")
|
||||
)
|
||||
let encodedInfo = info.bencode()
|
||||
let infoHash = MultiHash.digest($Sha1HashCodec, encodedInfo).mapFailure.tryGet()
|
||||
let torrentBytes = ("d4:info" & string.fromBytes(encodedInfo) & "e").toBytes()
|
||||
# let torrentBytesHex = byteutils.toHex(torrentBytes)
|
||||
|
||||
# check torrentBytesHex == "64343a696e666f64363a6c656e677468693130343835373665343a6e616d6531303a64617461314d2e62696e31323a7069656365206c656e6774686932363231343465363a70696563657338303a21feba308cd51e9acf88417193a9ea60f0f846463d4a8279853da2da355a574740217d446506e8eb1ad686b48b9560b15b8843fd00e7ec1b59624b095015e7da0c40350624c6b5a1fed1db39720b726c6565"
|
||||
|
||||
let infoBytes = extractInfoFromTorrent(torrentBytes).tryGet()
|
||||
|
||||
# echo string.fromBytes(infoBytes)
|
||||
|
||||
# let infoBytesHex = byteutils.toHex(infoBytes)
|
||||
|
||||
# check infoBytesHex == "64363a6c656e677468693130343835373665343a6e616d6531303a64617461314d2e62696e31323a7069656365206c656e6774686932363231343465363a70696563657338303a21feba308cd51e9acf88417193a9ea60f0f846463d4a8279853da2da355a574740217d446506e8eb1ad686b48b9560b15b8843fd00e7ec1b59624b095015e7da0c40350624c6b5a1fed1db39720b726c65"
|
||||
|
||||
let extractedInfoHash =
|
||||
MultiHash.digest($Sha1HashCodec, infoBytes).mapFailure.tryGet()
|
||||
|
||||
check extractedInfoHash == infoHash
|
||||
Loading…
x
Reference in New Issue
Block a user