enki b3204ea07a
Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
first commit
2025-08-18 00:40:15 -07:00

168 lines
4.0 KiB
Go

package torrent
import (
"crypto/sha1"
"fmt"
"net/url"
"github.com/anacrolix/torrent/bencode"
"github.com/anacrolix/torrent/metainfo"
)
type TorrentInfo struct {
InfoHash string
TorrentData []byte
Magnet string
}
type FileInfo struct {
Name string
Size int64
Pieces []PieceInfo
WebSeedURL string
}
type PieceInfo struct {
Index int
Hash [20]byte // SHA-1 hash for BitTorrent compatibility
SHA256 string // SHA-256 hash for Blossom
Length int
}
func CreateTorrent(fileInfo FileInfo, trackers []string, gatewayURL string, dhtNodes [][]interface{}) (*TorrentInfo, error) {
// Calculate piece length based on file size (following BUD-10 spec)
pieceLength := calculatePieceLength(fileInfo.Size)
// Create pieces buffer - concatenated SHA-1 hashes
var pieces []byte
for _, piece := range fileInfo.Pieces {
pieces = append(pieces, piece.Hash[:]...)
}
// Create metainfo
info := metainfo.Info{
Name: fileInfo.Name,
Length: fileInfo.Size,
PieceLength: pieceLength,
Pieces: pieces,
}
// Build announce list with gateway tracker first, then fallbacks
var announceList metainfo.AnnounceList
// Primary: Gateway's built-in tracker
if gatewayURL != "" {
gatewayTracker := fmt.Sprintf("%s/announce", gatewayURL)
announceList = append(announceList, []string{gatewayTracker})
}
// Fallbacks: External trackers
for _, tracker := range trackers {
announceList = append(announceList, []string{tracker})
}
// Primary announce URL (gateway tracker if available, otherwise first external)
primaryAnnounce := ""
if len(announceList) > 0 && len(announceList[0]) > 0 {
primaryAnnounce = announceList[0][0]
} else if len(trackers) > 0 {
primaryAnnounce = trackers[0]
}
// Convert DHT nodes to metainfo.Node format
var nodes []metainfo.Node
for _, nodeArray := range dhtNodes {
if len(nodeArray) >= 2 {
// Node format is "host:port" string
node := metainfo.Node(fmt.Sprintf("%v:%v", nodeArray[0], nodeArray[1]))
nodes = append(nodes, node)
}
}
mi := metainfo.MetaInfo{
InfoBytes: bencode.MustMarshal(info),
Announce: primaryAnnounce,
AnnounceList: announceList,
Nodes: nodes, // DHT bootstrap nodes (BEP-5)
}
// Add WebSeed support (BEP-19)
if fileInfo.WebSeedURL != "" {
mi.UrlList = []string{fileInfo.WebSeedURL}
}
// Calculate info hash
infoHash := mi.HashInfoBytes()
// Generate torrent data
torrentData, err := bencode.Marshal(mi)
if err != nil {
return nil, fmt.Errorf("error marshaling torrent: %w", err)
}
// Generate magnet link with all trackers
allTrackers := []string{}
for _, tier := range announceList {
allTrackers = append(allTrackers, tier...)
}
magnet := generateMagnetLink(infoHash, fileInfo.Name, allTrackers, fileInfo.WebSeedURL)
return &TorrentInfo{
InfoHash: fmt.Sprintf("%x", infoHash),
TorrentData: torrentData,
Magnet: magnet,
}, nil
}
func calculatePieceLength(fileSize int64) int64 {
// Following BUD-10 piece size strategy
const (
KB = 1024
MB = KB * 1024
GB = MB * 1024
)
switch {
case fileSize < 50*MB:
return 256 * KB
case fileSize < 500*MB:
return 512 * KB
case fileSize < 2*GB:
return 1 * MB
default:
return 2 * MB
}
}
func generateMagnetLink(infoHash [20]byte, name string, trackers []string, webSeedURL string) string {
params := url.Values{}
params.Set("xt", fmt.Sprintf("urn:btih:%x", infoHash))
params.Set("dn", name)
for _, tracker := range trackers {
params.Add("tr", tracker)
}
if webSeedURL != "" {
params.Set("ws", webSeedURL)
}
return "magnet:?" + params.Encode()
}
// ConvertSHA256ToSHA1 converts SHA-256 data to SHA-1 for BitTorrent compatibility
// This is used when we have chunk data and need both hashes
func ConvertSHA256ToSHA1(data []byte) [20]byte {
hash := sha1.Sum(data)
return hash
}
// CreatePieceInfo creates piece info from chunk data
func CreatePieceInfo(index int, data []byte, sha256Hash string) PieceInfo {
return PieceInfo{
Index: index,
Hash: ConvertSHA256ToSHA1(data),
SHA256: sha256Hash,
Length: len(data),
}
}