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), } }