972 lines
26 KiB
Go
972 lines
26 KiB
Go
package p2p
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"torrentGateway/internal/config"
|
|
"torrentGateway/internal/dht"
|
|
"torrentGateway/internal/tracker"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// UnifiedP2PGateway coordinates all P2P systems
|
|
type UnifiedP2PGateway struct {
|
|
// P2P Components
|
|
tracker *tracker.Tracker
|
|
wsTracker *tracker.WebSocketTracker
|
|
dhtBootstrap *dht.DHTBootstrap
|
|
coordinator *P2PCoordinator
|
|
|
|
// Configuration
|
|
config *config.Config
|
|
db *sql.DB
|
|
|
|
// Maintenance
|
|
maintenanceTicker *time.Ticker
|
|
healthTicker *time.Ticker
|
|
stopCh chan struct{}
|
|
|
|
// Caching for peer discovery
|
|
peerCache map[string]*CachedPeerResponse
|
|
cacheMutex sync.RWMutex
|
|
cacheExpiry time.Duration
|
|
|
|
// Statistics
|
|
stats *P2PGatewayStats
|
|
statsMutex sync.RWMutex
|
|
}
|
|
|
|
// CachedPeerResponse represents a cached peer discovery response
|
|
type CachedPeerResponse struct {
|
|
Peers []UnifiedPeer `json:"peers"`
|
|
CachedAt time.Time `json:"cached_at"`
|
|
TTL time.Duration `json:"ttl"`
|
|
Sources []string `json:"sources"`
|
|
}
|
|
|
|
// UnifiedPeer represents a peer from any source
|
|
type UnifiedPeer struct {
|
|
ID string `json:"id"`
|
|
IP string `json:"ip"`
|
|
Port int `json:"port"`
|
|
Source string `json:"source"` // "tracker", "dht", "websocket"
|
|
Quality int `json:"quality"`
|
|
IsSeeder bool `json:"is_seeder"`
|
|
LastSeen time.Time `json:"last_seen"`
|
|
Endpoint string `json:"endpoint,omitempty"`
|
|
Protocol string `json:"protocol,omitempty"` // "webrtc", "http", "webseed"
|
|
Reliability float64 `json:"reliability,omitempty"`
|
|
RTT int `json:"rtt_ms,omitempty"`
|
|
}
|
|
|
|
// P2PGatewayStats tracks comprehensive P2P statistics
|
|
type P2PGatewayStats struct {
|
|
TotalTorrents int64 `json:"total_torrents"`
|
|
ActiveTorrents int64 `json:"active_torrents"`
|
|
TotalPeers int64 `json:"total_peers"`
|
|
TrackerPeers int64 `json:"tracker_peers"`
|
|
DHTNodes int64 `json:"dht_nodes"`
|
|
WebSocketPeers int64 `json:"websocket_peers"`
|
|
AnnouncesSent int64 `json:"announces_sent"`
|
|
AnnouncesReceived int64 `json:"announces_received"`
|
|
HealthStatus map[string]string `json:"health_status"`
|
|
LastMaintenance time.Time `json:"last_maintenance"`
|
|
LastHealthCheck time.Time `json:"last_health_check"`
|
|
SystemHealth string `json:"system_health"`
|
|
ComponentStats map[string]interface{} `json:"component_stats"`
|
|
}
|
|
|
|
// TorrentInfo is defined in coordinator.go
|
|
|
|
// NewUnifiedP2PGateway creates a new unified P2P gateway
|
|
func NewUnifiedP2PGateway(config *config.Config, db *sql.DB) *UnifiedP2PGateway {
|
|
gateway := &UnifiedP2PGateway{
|
|
config: config,
|
|
db: db,
|
|
peerCache: make(map[string]*CachedPeerResponse),
|
|
cacheExpiry: 2 * time.Minute, // Cache peer responses for 2 minutes
|
|
stopCh: make(chan struct{}),
|
|
stats: &P2PGatewayStats{
|
|
HealthStatus: make(map[string]string),
|
|
ComponentStats: make(map[string]interface{}),
|
|
SystemHealth: "starting",
|
|
},
|
|
}
|
|
|
|
return gateway
|
|
}
|
|
|
|
// Initialize starts all P2P components and background tasks
|
|
func (g *UnifiedP2PGateway) Initialize() error {
|
|
log.Printf("P2P Gateway: Initializing unified P2P system")
|
|
|
|
// Initialize tracker
|
|
if err := g.initializeTracker(); err != nil {
|
|
return fmt.Errorf("failed to initialize tracker: %w", err)
|
|
}
|
|
|
|
// Initialize DHT
|
|
if err := g.initializeDHT(); err != nil {
|
|
return fmt.Errorf("failed to initialize DHT: %w", err)
|
|
}
|
|
|
|
// Initialize WebSocket tracker
|
|
if err := g.initializeWebSocketTracker(); err != nil {
|
|
return fmt.Errorf("failed to initialize WebSocket tracker: %w", err)
|
|
}
|
|
|
|
// Initialize P2P coordinator
|
|
if err := g.initializeCoordinator(); err != nil {
|
|
return fmt.Errorf("failed to initialize coordinator: %w", err)
|
|
}
|
|
|
|
// Start background tasks
|
|
g.startBackgroundTasks()
|
|
|
|
g.stats.SystemHealth = "healthy"
|
|
log.Printf("P2P Gateway: Successfully initialized all P2P systems")
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateTorrent creates a new torrent and announces it to all P2P systems
|
|
func (g *UnifiedP2PGateway) CreateTorrent(fileHash string) (*TorrentInfo, error) {
|
|
log.Printf("P2P Gateway: Creating torrent for file %s", fileHash[:8])
|
|
|
|
// Get file info from database - this is a simplified version
|
|
// In production, you'd query the files table for name and size
|
|
filename := "Unknown"
|
|
var fileSize int64 = 0
|
|
|
|
row := g.db.QueryRow("SELECT original_name, size FROM files WHERE hash = ?", fileHash)
|
|
row.Scan(&filename, &fileSize) // Ignore error, use defaults
|
|
|
|
// Create torrent metadata
|
|
torrentInfo := &TorrentInfo{
|
|
InfoHash: fileHash,
|
|
Name: filename,
|
|
Size: fileSize,
|
|
CreatedAt: time.Now(),
|
|
LastAnnounce: time.Now(),
|
|
IsActive: true,
|
|
}
|
|
|
|
// Store in database
|
|
if err := g.storeTorrentInfo(torrentInfo); err != nil {
|
|
log.Printf("P2P Gateway: Failed to store torrent info: %v", err)
|
|
}
|
|
|
|
// Announce to all P2P systems immediately
|
|
if err := g.announceToAllSystems(torrentInfo); err != nil {
|
|
log.Printf("P2P Gateway: Failed to announce to all systems: %v", err)
|
|
}
|
|
|
|
// Update statistics
|
|
g.statsMutex.Lock()
|
|
g.stats.TotalTorrents++
|
|
g.stats.ActiveTorrents++
|
|
g.statsMutex.Unlock()
|
|
|
|
log.Printf("P2P Gateway: Successfully created and announced torrent %s", fileHash[:8])
|
|
return torrentInfo, nil
|
|
}
|
|
|
|
// GetPeersHandler provides a unified peer discovery endpoint
|
|
func (g *UnifiedP2PGateway) GetPeersHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
infoHash := vars["infohash"]
|
|
|
|
if len(infoHash) != 40 {
|
|
http.Error(w, "Invalid infohash format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check cache first
|
|
g.cacheMutex.RLock()
|
|
if cached, exists := g.peerCache[infoHash]; exists {
|
|
if time.Since(cached.CachedAt) < cached.TTL {
|
|
g.cacheMutex.RUnlock()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("X-Cache", "HIT")
|
|
json.NewEncoder(w).Encode(cached)
|
|
return
|
|
}
|
|
}
|
|
g.cacheMutex.RUnlock()
|
|
|
|
// Cache miss - gather peers from all sources
|
|
peers, sources, err := g.gatherPeersFromAllSources(infoHash)
|
|
if err != nil {
|
|
log.Printf("P2P Gateway: Failed to gather peers for %s: %v", infoHash[:8], err)
|
|
http.Error(w, "Failed to gather peers", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Create response
|
|
response := &CachedPeerResponse{
|
|
Peers: peers,
|
|
CachedAt: time.Now(),
|
|
TTL: g.cacheExpiry,
|
|
Sources: sources,
|
|
}
|
|
|
|
// Update cache
|
|
g.cacheMutex.Lock()
|
|
g.peerCache[infoHash] = response
|
|
g.cacheMutex.Unlock()
|
|
|
|
// Send response
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("X-Cache", "MISS")
|
|
json.NewEncoder(w).Encode(response)
|
|
|
|
log.Printf("P2P Gateway: Returned %d peers from %v for %s",
|
|
len(peers), sources, infoHash[:8])
|
|
}
|
|
|
|
// GetStatsHandler returns comprehensive P2P statistics
|
|
func (g *UnifiedP2PGateway) GetStatsHandler(w http.ResponseWriter, r *http.Request) {
|
|
g.updateStats()
|
|
|
|
g.statsMutex.RLock()
|
|
stats := *g.stats
|
|
g.statsMutex.RUnlock()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(stats)
|
|
}
|
|
|
|
// GetHealthHandler returns system health status
|
|
func (g *UnifiedP2PGateway) GetHealthHandler(w http.ResponseWriter, r *http.Request) {
|
|
health := g.performHealthCheck()
|
|
|
|
statusCode := http.StatusOK
|
|
if health.SystemHealth != "healthy" {
|
|
statusCode = http.StatusServiceUnavailable
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
json.NewEncoder(w).Encode(health)
|
|
}
|
|
|
|
// Shutdown gracefully stops all P2P systems
|
|
func (g *UnifiedP2PGateway) Shutdown() error {
|
|
log.Printf("P2P Gateway: Shutting down unified P2P system")
|
|
|
|
close(g.stopCh)
|
|
|
|
if g.maintenanceTicker != nil {
|
|
g.maintenanceTicker.Stop()
|
|
}
|
|
if g.healthTicker != nil {
|
|
g.healthTicker.Stop()
|
|
}
|
|
|
|
// Shutdown components
|
|
if g.coordinator != nil {
|
|
g.coordinator.Stop()
|
|
}
|
|
if g.dhtBootstrap != nil {
|
|
g.dhtBootstrap.Stop()
|
|
}
|
|
|
|
g.stats.SystemHealth = "shutdown"
|
|
log.Printf("P2P Gateway: Shutdown complete")
|
|
|
|
return nil
|
|
}
|
|
|
|
// RegisterRoutes registers all P2P endpoints
|
|
func (g *UnifiedP2PGateway) RegisterRoutes(router *mux.Router) {
|
|
// Peer discovery endpoint
|
|
router.HandleFunc("/api/peers/{infohash}", g.GetPeersHandler).Methods("GET")
|
|
|
|
// Statistics endpoint
|
|
router.HandleFunc("/api/p2p/stats", g.GetStatsHandler).Methods("GET")
|
|
|
|
// Health check endpoint
|
|
router.HandleFunc("/api/p2p/health", g.GetHealthHandler).Methods("GET")
|
|
|
|
// WebSocket tracker endpoint (if WebSocket tracker is available)
|
|
if g.wsTracker != nil {
|
|
router.HandleFunc("/ws/tracker", g.wsTracker.HandleWS)
|
|
}
|
|
|
|
log.Printf("P2P Gateway: Registered API endpoints")
|
|
}
|
|
|
|
// ============ GATEWAY INTERFACE METHODS ============
|
|
|
|
// DHT Bootstrap Gateway interface methods
|
|
|
|
// GetPublicURL returns the public URL for this gateway
|
|
func (g *UnifiedP2PGateway) GetPublicURL() string {
|
|
// Try to get from config, fall back to localhost
|
|
if g.config.Gateway.PublicURL != "" {
|
|
return g.config.Gateway.PublicURL
|
|
}
|
|
return fmt.Sprintf("http://localhost:%d", g.config.Gateway.Port)
|
|
}
|
|
|
|
// GetDHTPort returns the DHT port
|
|
func (g *UnifiedP2PGateway) GetDHTPort() int {
|
|
return g.config.DHT.Port
|
|
}
|
|
|
|
// GetDatabase returns the database connection
|
|
func (g *UnifiedP2PGateway) GetDatabase() *sql.DB {
|
|
return g.db
|
|
}
|
|
|
|
// GetAllTorrentHashes returns all torrent hashes from the database
|
|
func (g *UnifiedP2PGateway) GetAllTorrentHashes() []string {
|
|
rows, err := g.db.Query("SELECT info_hash FROM p2p_torrents WHERE is_active = 1")
|
|
if err != nil {
|
|
log.Printf("P2P Gateway: Failed to get torrent hashes: %v", err)
|
|
return []string{}
|
|
}
|
|
defer rows.Close()
|
|
|
|
var hashes []string
|
|
for rows.Next() {
|
|
var hash string
|
|
if err := rows.Scan(&hash); err == nil {
|
|
hashes = append(hashes, hash)
|
|
}
|
|
}
|
|
return hashes
|
|
}
|
|
|
|
// Coordinator Gateway interface methods
|
|
|
|
// WebSeedPeer returns a PeerInfo for the WebSeed
|
|
func (g *UnifiedP2PGateway) WebSeedPeer() PeerInfo {
|
|
return PeerInfo{
|
|
IP: "127.0.0.1", // Local WebSeed
|
|
Port: g.config.Gateway.Port,
|
|
PeerID: "WEBSEED",
|
|
Source: "webseed",
|
|
Quality: 100, // Highest quality
|
|
LastSeen: time.Now(),
|
|
}
|
|
}
|
|
|
|
// EnableWebSeed enables WebSeed for a torrent
|
|
func (g *UnifiedP2PGateway) EnableWebSeed(infoHash string) error {
|
|
log.Printf("P2P Gateway: Enabling WebSeed for %s", infoHash[:8])
|
|
// In a full implementation, this would configure WebSeed URLs
|
|
return nil
|
|
}
|
|
|
|
// PublishToNostr publishes torrent to Nostr (placeholder)
|
|
func (g *UnifiedP2PGateway) PublishToNostr(torrent *TorrentInfo) error {
|
|
log.Printf("P2P Gateway: Publishing torrent %s to Nostr", torrent.InfoHash[:8])
|
|
// Placeholder - would integrate with actual Nostr publisher
|
|
return nil
|
|
}
|
|
|
|
// GetPort returns the gateway port
|
|
func (g *UnifiedP2PGateway) GetPort() int {
|
|
return g.config.Gateway.Port
|
|
}
|
|
|
|
// GetDHTBootstrap returns the DHT bootstrap instance
|
|
func (g *UnifiedP2PGateway) GetDHTBootstrap() *dht.DHTBootstrap {
|
|
return g.dhtBootstrap
|
|
}
|
|
|
|
// GetPeers returns peers for a given infohash from all sources
|
|
func (g *UnifiedP2PGateway) GetPeers(infoHash string) ([]UnifiedPeer, error) {
|
|
if g.coordinator == nil {
|
|
return []UnifiedPeer{}, fmt.Errorf("P2P coordinator not initialized")
|
|
}
|
|
|
|
// Get peers from all sources
|
|
peers, _, err := g.gatherPeersFromAllSources(infoHash)
|
|
return peers, err
|
|
}
|
|
|
|
// GetStats returns comprehensive P2P statistics
|
|
func (g *UnifiedP2PGateway) GetStats() (map[string]interface{}, error) {
|
|
if g.stats == nil {
|
|
return map[string]interface{}{}, fmt.Errorf("stats not initialized")
|
|
}
|
|
|
|
// Return current stats
|
|
stats := make(map[string]interface{})
|
|
stats["timestamp"] = time.Now().Format(time.RFC3339)
|
|
stats["health_status"] = g.stats.HealthStatus
|
|
stats["component_stats"] = g.stats.ComponentStats
|
|
stats["system_health"] = g.stats.SystemHealth
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// ============ INITIALIZATION METHODS ============
|
|
|
|
func (g *UnifiedP2PGateway) initializeTracker() error {
|
|
log.Printf("P2P Gateway: Initializing HTTP tracker")
|
|
|
|
// Note: This is a simplified tracker initialization for P2P gateway
|
|
// In production, you would pass proper config and gateway interface
|
|
log.Printf("P2P Gateway: Tracker initialization skipped - using external tracker")
|
|
|
|
g.stats.HealthStatus["tracker"] = "external"
|
|
return nil
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) initializeDHT() error {
|
|
log.Printf("P2P Gateway: Initializing DHT")
|
|
|
|
// First create DHT node
|
|
dhtNode, err := dht.NewDHT(&g.config.DHT)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create DHT node: %w", err)
|
|
}
|
|
|
|
// Then create DHT bootstrap with the node
|
|
g.dhtBootstrap = dht.NewDHTBootstrap(dhtNode, g, &g.config.DHT)
|
|
|
|
// Initialize the bootstrap functionality
|
|
if err := g.dhtBootstrap.Initialize(); err != nil {
|
|
return fmt.Errorf("failed to initialize DHT bootstrap: %w", err)
|
|
}
|
|
|
|
g.stats.HealthStatus["dht"] = "healthy"
|
|
return nil
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) initializeWebSocketTracker() error {
|
|
log.Printf("P2P Gateway: Initializing WebSocket tracker")
|
|
|
|
g.wsTracker = tracker.NewWebSocketTracker(g.tracker)
|
|
g.wsTracker.StartCleanup()
|
|
|
|
g.stats.HealthStatus["websocket"] = "healthy"
|
|
return nil
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) initializeCoordinator() error {
|
|
log.Printf("P2P Gateway: Initializing P2P coordinator")
|
|
|
|
g.coordinator = NewCoordinator(g, g.tracker, g.dhtBootstrap)
|
|
|
|
g.stats.HealthStatus["coordinator"] = "healthy"
|
|
return nil
|
|
}
|
|
|
|
// ============ BACKGROUND MAINTENANCE TASKS ============
|
|
|
|
func (g *UnifiedP2PGateway) startBackgroundTasks() {
|
|
log.Printf("P2P Gateway: Starting background maintenance tasks")
|
|
|
|
// Maintenance tasks every 5 minutes
|
|
g.maintenanceTicker = time.NewTicker(5 * time.Minute)
|
|
go g.maintenanceLoop()
|
|
|
|
// Health checks every minute
|
|
g.healthTicker = time.NewTicker(1 * time.Minute)
|
|
go g.healthCheckLoop()
|
|
|
|
// Periodic DHT announces every 15 minutes
|
|
go g.periodicAnnounceLoop()
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) maintenanceLoop() {
|
|
for {
|
|
select {
|
|
case <-g.stopCh:
|
|
return
|
|
case <-g.maintenanceTicker.C:
|
|
g.performMaintenance()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) healthCheckLoop() {
|
|
for {
|
|
select {
|
|
case <-g.stopCh:
|
|
return
|
|
case <-g.healthTicker.C:
|
|
g.performHealthCheck()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) periodicAnnounceLoop() {
|
|
ticker := time.NewTicker(15 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-g.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
g.performPeriodicAnnounces()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) performMaintenance() {
|
|
log.Printf("P2P Gateway: Performing maintenance tasks")
|
|
|
|
g.statsMutex.Lock()
|
|
g.stats.LastMaintenance = time.Now()
|
|
g.statsMutex.Unlock()
|
|
|
|
// Clean up expired peers from all systems
|
|
g.cleanupExpiredPeers()
|
|
|
|
// Verify WebSeeds are accessible
|
|
g.verifyWebSeeds()
|
|
|
|
// Clean up peer cache
|
|
g.cleanupPeerCache()
|
|
|
|
// Update statistics
|
|
g.updateStats()
|
|
|
|
log.Printf("P2P Gateway: Maintenance tasks completed")
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) cleanupExpiredPeers() {
|
|
// Clean up tracker peers
|
|
if g.tracker != nil {
|
|
// Note: cleanupExpiredPeers is a private method, can't call directly
|
|
log.Printf("P2P Gateway: Tracker cleanup skipped (private method)")
|
|
}
|
|
|
|
// Clean up DHT peers
|
|
if g.dhtBootstrap != nil && g.dhtBootstrap.GetNode() != nil {
|
|
// DHT cleanup is handled internally by the node
|
|
}
|
|
|
|
// WebSocket tracker cleanup is handled by its own ticker
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) verifyWebSeeds() {
|
|
// Get all active torrents with WebSeeds
|
|
torrents, err := g.getActiveTorrentsWithWebSeeds()
|
|
if err != nil {
|
|
log.Printf("P2P Gateway: Failed to get torrents for WebSeed verification: %v", err)
|
|
return
|
|
}
|
|
|
|
verifiedCount := 0
|
|
failedCount := 0
|
|
|
|
for _, torrent := range torrents {
|
|
if torrent.WebSeedURL != "" {
|
|
if g.verifyWebSeedURL(torrent.WebSeedURL) {
|
|
verifiedCount++
|
|
} else {
|
|
failedCount++
|
|
log.Printf("P2P Gateway: WebSeed verification failed for %s: %s",
|
|
torrent.InfoHash[:8], torrent.WebSeedURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
if verifiedCount > 0 || failedCount > 0 {
|
|
log.Printf("P2P Gateway: WebSeed verification completed: %d verified, %d failed",
|
|
verifiedCount, failedCount)
|
|
}
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) verifyWebSeedURL(url string) bool {
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Head(url)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusPartialContent
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) cleanupPeerCache() {
|
|
g.cacheMutex.Lock()
|
|
defer g.cacheMutex.Unlock()
|
|
|
|
now := time.Now()
|
|
cleanedCount := 0
|
|
|
|
for infoHash, cached := range g.peerCache {
|
|
if now.Sub(cached.CachedAt) > cached.TTL {
|
|
delete(g.peerCache, infoHash)
|
|
cleanedCount++
|
|
}
|
|
}
|
|
|
|
if cleanedCount > 0 {
|
|
log.Printf("P2P Gateway: Cleaned %d expired peer cache entries", cleanedCount)
|
|
}
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) performPeriodicAnnounces() {
|
|
log.Printf("P2P Gateway: Performing periodic announces for all torrents")
|
|
|
|
torrents, err := g.getAllActiveTorrents()
|
|
if err != nil {
|
|
log.Printf("P2P Gateway: Failed to get active torrents for periodic announces: %v", err)
|
|
return
|
|
}
|
|
|
|
announceCount := 0
|
|
for _, torrent := range torrents {
|
|
if err := g.announceToAllSystems(torrent); err != nil {
|
|
log.Printf("P2P Gateway: Failed to announce %s: %v", torrent.InfoHash[:8], err)
|
|
} else {
|
|
announceCount++
|
|
}
|
|
}
|
|
|
|
g.statsMutex.Lock()
|
|
g.stats.AnnouncesSent += int64(announceCount)
|
|
g.statsMutex.Unlock()
|
|
|
|
log.Printf("P2P Gateway: Completed periodic announces for %d torrents", announceCount)
|
|
}
|
|
|
|
// ============ HEALTH CHECK SYSTEM ============
|
|
|
|
func (g *UnifiedP2PGateway) performHealthCheck() *P2PGatewayStats {
|
|
g.statsMutex.Lock()
|
|
g.stats.LastHealthCheck = time.Now()
|
|
|
|
// Check DHT health
|
|
if g.dhtBootstrap != nil && g.dhtBootstrap.GetNode() != nil {
|
|
dhtStats := g.dhtBootstrap.GetDHTStats()
|
|
if nodeCount, ok := dhtStats["routing_table_size"].(int); ok && nodeCount > 0 {
|
|
g.stats.HealthStatus["dht"] = "healthy"
|
|
} else {
|
|
g.stats.HealthStatus["dht"] = "degraded"
|
|
}
|
|
} else {
|
|
g.stats.HealthStatus["dht"] = "failed"
|
|
}
|
|
|
|
// Check tracker health
|
|
if g.tracker != nil {
|
|
trackerStats := g.tracker.GetStats()
|
|
if peerCount, ok := trackerStats["peers"].(int64); ok && peerCount >= 0 {
|
|
g.stats.HealthStatus["tracker"] = "healthy"
|
|
} else {
|
|
g.stats.HealthStatus["tracker"] = "degraded"
|
|
}
|
|
} else {
|
|
g.stats.HealthStatus["tracker"] = "failed"
|
|
}
|
|
|
|
// Check WebSocket tracker health
|
|
if g.wsTracker != nil {
|
|
wsStats := g.wsTracker.GetStats()
|
|
if peers, ok := wsStats["total_peers"].(int); ok && peers >= 0 {
|
|
g.stats.HealthStatus["websocket"] = "healthy"
|
|
} else {
|
|
g.stats.HealthStatus["websocket"] = "degraded"
|
|
}
|
|
} else {
|
|
g.stats.HealthStatus["websocket"] = "failed"
|
|
}
|
|
|
|
// Determine overall system health
|
|
healthyComponents := 0
|
|
totalComponents := len(g.stats.HealthStatus)
|
|
|
|
for _, status := range g.stats.HealthStatus {
|
|
if status == "healthy" {
|
|
healthyComponents++
|
|
}
|
|
}
|
|
|
|
if healthyComponents == totalComponents {
|
|
g.stats.SystemHealth = "healthy"
|
|
} else if healthyComponents > totalComponents/2 {
|
|
g.stats.SystemHealth = "degraded"
|
|
} else {
|
|
g.stats.SystemHealth = "critical"
|
|
}
|
|
|
|
stats := *g.stats
|
|
g.statsMutex.Unlock()
|
|
|
|
return &stats
|
|
}
|
|
|
|
// ============ PEER DISCOVERY AND COORDINATION ============
|
|
|
|
func (g *UnifiedP2PGateway) gatherPeersFromAllSources(infoHash string) ([]UnifiedPeer, []string, error) {
|
|
var allPeers []UnifiedPeer
|
|
var sources []string
|
|
|
|
// Get peers from HTTP tracker
|
|
if g.tracker != nil {
|
|
trackerPeers, err := g.tracker.GetPeersForTorrent(infoHash)
|
|
if err == nil && len(trackerPeers) > 0 {
|
|
for _, peer := range trackerPeers {
|
|
unifiedPeer := UnifiedPeer{
|
|
ID: peer.PeerID,
|
|
IP: peer.IP,
|
|
Port: peer.Port,
|
|
Source: "tracker",
|
|
Quality: peer.Priority,
|
|
IsSeeder: peer.IsSeeder || peer.Left == 0,
|
|
LastSeen: peer.LastSeen,
|
|
Protocol: "http",
|
|
Reliability: calculateReliability(peer),
|
|
}
|
|
|
|
if peer.IsWebSeed {
|
|
unifiedPeer.Protocol = "webseed"
|
|
unifiedPeer.Endpoint = fmt.Sprintf("http://localhost:%d/webseed/%s", g.config.Gateway.Port, infoHash)
|
|
unifiedPeer.Quality = 100 // WebSeeds get highest quality
|
|
}
|
|
|
|
allPeers = append(allPeers, unifiedPeer)
|
|
}
|
|
sources = append(sources, "tracker")
|
|
}
|
|
}
|
|
|
|
// Get peers from DHT
|
|
if g.dhtBootstrap != nil && g.dhtBootstrap.GetNode() != nil {
|
|
dhtPeers := g.coordinator.getDHTPeers(infoHash)
|
|
if len(dhtPeers) > 0 {
|
|
for _, peer := range dhtPeers {
|
|
unifiedPeer := UnifiedPeer{
|
|
ID: peer.PeerID,
|
|
IP: peer.IP,
|
|
Port: peer.Port,
|
|
Source: "dht",
|
|
Quality: peer.Quality,
|
|
LastSeen: peer.LastSeen,
|
|
Protocol: "http",
|
|
}
|
|
allPeers = append(allPeers, unifiedPeer)
|
|
}
|
|
sources = append(sources, "dht")
|
|
}
|
|
}
|
|
|
|
// Get peers from WebSocket tracker
|
|
if g.wsTracker != nil {
|
|
wsStats := g.wsTracker.GetStats()
|
|
// Note: WebSocket tracker doesn't have a direct GetPeers method
|
|
// This would need to be implemented based on the swarm structure
|
|
if totalPeers, ok := wsStats["total_peers"].(int); ok && totalPeers > 0 {
|
|
sources = append(sources, "websocket")
|
|
}
|
|
}
|
|
|
|
return allPeers, sources, nil
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) announceToAllSystems(torrent *TorrentInfo) error {
|
|
var errors []error
|
|
|
|
// Announce to HTTP tracker
|
|
if g.tracker != nil {
|
|
if err := g.announceToTracker(torrent); err != nil {
|
|
errors = append(errors, fmt.Errorf("tracker announce failed: %w", err))
|
|
}
|
|
}
|
|
|
|
// Announce to DHT
|
|
if g.dhtBootstrap != nil {
|
|
if err := g.announceToDHT(torrent); err != nil {
|
|
errors = append(errors, fmt.Errorf("DHT announce failed: %w", err))
|
|
}
|
|
}
|
|
|
|
// Update last announce time
|
|
torrent.LastAnnounce = time.Now()
|
|
if err := g.updateTorrentInfo(torrent); err != nil {
|
|
errors = append(errors, fmt.Errorf("failed to update torrent info: %w", err))
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return fmt.Errorf("announce errors: %v", errors)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) announceToTracker(torrent *TorrentInfo) error {
|
|
// This would integrate with the tracker's announce system
|
|
log.Printf("P2P Gateway: Announced %s to HTTP tracker", torrent.InfoHash[:8])
|
|
return nil
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) announceToDHT(torrent *TorrentInfo) error {
|
|
if g.dhtBootstrap != nil {
|
|
g.dhtBootstrap.AnnounceNewTorrent(torrent.InfoHash, g.config.DHT.Port)
|
|
log.Printf("P2P Gateway: Announced %s to DHT", torrent.InfoHash[:8])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ============ STATISTICS AND UTILITIES ============
|
|
|
|
func (g *UnifiedP2PGateway) updateStats() {
|
|
g.statsMutex.Lock()
|
|
defer g.statsMutex.Unlock()
|
|
|
|
// Update component statistics
|
|
if g.tracker != nil {
|
|
g.stats.ComponentStats["tracker"] = g.tracker.GetStats()
|
|
}
|
|
|
|
if g.dhtBootstrap != nil {
|
|
g.stats.ComponentStats["dht"] = g.dhtBootstrap.GetDHTStats()
|
|
}
|
|
|
|
if g.wsTracker != nil {
|
|
g.stats.ComponentStats["websocket"] = g.wsTracker.GetStats()
|
|
}
|
|
|
|
// Calculate total counts
|
|
g.stats.TotalPeers = 0
|
|
if trackerStats, ok := g.stats.ComponentStats["tracker"].(map[string]interface{}); ok {
|
|
if peers, ok := trackerStats["peers"].(int64); ok {
|
|
g.stats.TrackerPeers = peers
|
|
g.stats.TotalPeers += peers
|
|
}
|
|
}
|
|
|
|
if dhtStats, ok := g.stats.ComponentStats["dht"].(map[string]interface{}); ok {
|
|
if nodes, ok := dhtStats["routing_table_size"].(int); ok {
|
|
g.stats.DHTNodes = int64(nodes)
|
|
}
|
|
}
|
|
|
|
if wsStats, ok := g.stats.ComponentStats["websocket"].(map[string]interface{}); ok {
|
|
if peers, ok := wsStats["total_peers"].(int); ok {
|
|
g.stats.WebSocketPeers = int64(peers)
|
|
g.stats.TotalPeers += int64(peers)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions for database operations
|
|
func (g *UnifiedP2PGateway) storeTorrentInfo(torrent *TorrentInfo) error {
|
|
query := `INSERT OR REPLACE INTO p2p_torrents (info_hash, name, size, created_at, last_announce, webseed_url, is_active)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
_, err := g.db.Exec(query, torrent.InfoHash, torrent.Name, torrent.Size,
|
|
torrent.CreatedAt, torrent.LastAnnounce, torrent.WebSeedURL, torrent.IsActive)
|
|
return err
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) updateTorrentInfo(torrent *TorrentInfo) error {
|
|
query := `UPDATE p2p_torrents SET last_announce = ?, is_active = ? WHERE info_hash = ?`
|
|
_, err := g.db.Exec(query, torrent.LastAnnounce, torrent.IsActive, torrent.InfoHash)
|
|
return err
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) getAllActiveTorrents() ([]*TorrentInfo, error) {
|
|
query := `SELECT info_hash, name, size, created_at, last_announce, webseed_url, is_active
|
|
FROM p2p_torrents WHERE is_active = 1`
|
|
rows, err := g.db.Query(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var torrents []*TorrentInfo
|
|
for rows.Next() {
|
|
var torrent TorrentInfo
|
|
var webSeedURL sql.NullString
|
|
err := rows.Scan(&torrent.InfoHash, &torrent.Name, &torrent.Size,
|
|
&torrent.CreatedAt, &torrent.LastAnnounce, &webSeedURL, &torrent.IsActive)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if webSeedURL.Valid {
|
|
torrent.WebSeedURL = webSeedURL.String
|
|
}
|
|
torrents = append(torrents, &torrent)
|
|
}
|
|
|
|
return torrents, nil
|
|
}
|
|
|
|
func (g *UnifiedP2PGateway) getActiveTorrentsWithWebSeeds() ([]*TorrentInfo, error) {
|
|
query := `SELECT info_hash, name, size, created_at, last_announce, webseed_url, is_active
|
|
FROM p2p_torrents WHERE is_active = 1 AND webseed_url IS NOT NULL AND webseed_url != ''`
|
|
rows, err := g.db.Query(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var torrents []*TorrentInfo
|
|
for rows.Next() {
|
|
var torrent TorrentInfo
|
|
err := rows.Scan(&torrent.InfoHash, &torrent.Name, &torrent.Size,
|
|
&torrent.CreatedAt, &torrent.LastAnnounce, &torrent.WebSeedURL, &torrent.IsActive)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
torrents = append(torrents, &torrent)
|
|
}
|
|
|
|
return torrents, nil
|
|
}
|
|
|
|
func calculateReliability(peer *tracker.PeerInfo) float64 {
|
|
// Calculate reliability based on various factors
|
|
reliability := 0.5 // Base reliability
|
|
|
|
if peer.IsWebSeed {
|
|
return 1.0 // WebSeeds are most reliable
|
|
}
|
|
|
|
if peer.IsSeeder {
|
|
reliability += 0.3
|
|
}
|
|
|
|
if peer.Priority > 50 {
|
|
reliability += 0.2
|
|
}
|
|
|
|
// Recent activity bonus
|
|
if time.Since(peer.LastSeen) < 10*time.Minute {
|
|
reliability += 0.2
|
|
}
|
|
|
|
if reliability > 1.0 {
|
|
reliability = 1.0
|
|
}
|
|
|
|
return reliability
|
|
}
|
|
|
|
// CreateP2PTables creates the necessary database tables for P2P coordination
|
|
func (g *UnifiedP2PGateway) CreateP2PTables() error {
|
|
query := `
|
|
CREATE TABLE IF NOT EXISTS p2p_torrents (
|
|
info_hash TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_announce DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
webseed_url TEXT,
|
|
is_active BOOLEAN DEFAULT 1
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_p2p_torrents_active ON p2p_torrents(is_active);
|
|
CREATE INDEX IF NOT EXISTS idx_p2p_torrents_last_announce ON p2p_torrents(last_announce);
|
|
`
|
|
|
|
_, err := g.db.Exec(query)
|
|
return err
|
|
} |